mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-19 22:47:21 +00:00
Compare commits
23 Commits
13363-acce
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72ad83532 | ||
|
|
f2aba45dfe | ||
|
|
639c2ce077 | ||
|
|
1bddc02ae0 | ||
|
|
ebea5176e2 | ||
|
|
39f550cf9f | ||
|
|
cdcbd00a92 | ||
|
|
86cd0e81ad | ||
|
|
6efe444af3 | ||
|
|
09c9665b2f | ||
|
|
23e394fec9 | ||
|
|
4b02a11634 | ||
|
|
ed9c00cab5 | ||
|
|
eaa1fb4107 | ||
|
|
c0a49b3d0b | ||
|
|
a7a00228a2 | ||
|
|
c85f7a71b2 | ||
|
|
c95e914219 | ||
|
|
362a0b96ab | ||
|
|
ef984fc438 | ||
|
|
ce9bbc9972 | ||
|
|
a3921f0559 | ||
|
|
f8ec5d27a4 |
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -154,7 +154,7 @@ try {
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
defaultSemverRangePrefix: ""
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
@@ -20,8 +20,6 @@ module.exports.ignorePackages = () => [];
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
[
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
require.resolve(
|
||||
|
||||
@@ -168,12 +168,16 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
),
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle
|
||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||
.join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
new StatsWriterPlugin({
|
||||
@@ -217,6 +221,42 @@ const createRspackConfig = ({
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
"@formatjs/intl-durationformat/should-polyfill$":
|
||||
"@formatjs/intl-durationformat/should-polyfill.js",
|
||||
"@formatjs/intl-durationformat/polyfill-force$":
|
||||
"@formatjs/intl-durationformat/polyfill-force.js",
|
||||
"@formatjs/intl-datetimeformat/should-polyfill":
|
||||
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-datetimeformat/polyfill-force":
|
||||
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
||||
"@formatjs/intl-displaynames/should-polyfill":
|
||||
"@formatjs/intl-displaynames/should-polyfill.js",
|
||||
"@formatjs/intl-displaynames/polyfill-force":
|
||||
"@formatjs/intl-displaynames/polyfill-force.js",
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
||||
"@formatjs/intl-listformat/should-polyfill":
|
||||
"@formatjs/intl-listformat/should-polyfill.js",
|
||||
"@formatjs/intl-listformat/polyfill-force":
|
||||
"@formatjs/intl-listformat/polyfill-force.js",
|
||||
"@formatjs/intl-locale/should-polyfill":
|
||||
"@formatjs/intl-locale/should-polyfill.js",
|
||||
"@formatjs/intl-locale/polyfill-force":
|
||||
"@formatjs/intl-locale/polyfill-force.js",
|
||||
"@formatjs/intl-numberformat/should-polyfill":
|
||||
"@formatjs/intl-numberformat/should-polyfill.js",
|
||||
"@formatjs/intl-numberformat/polyfill-force":
|
||||
"@formatjs/intl-numberformat/polyfill-force.js",
|
||||
"@formatjs/intl-pluralrules/should-polyfill":
|
||||
"@formatjs/intl-pluralrules/should-polyfill.js",
|
||||
"@formatjs/intl-pluralrules/polyfill-force":
|
||||
"@formatjs/intl-pluralrules/polyfill-force.js",
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill":
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -5,17 +5,19 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { framework } from "./cast_framework";
|
||||
import { CAST_NS } from "../../../src/cast/const";
|
||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||
import "../../../src/resources/custom-card-support";
|
||||
import { castContext } from "./cast_context";
|
||||
import { framework } from "./cast_framework";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import type { ReceivedMessage } from "./types";
|
||||
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
@@ -40,7 +39,8 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -89,31 +89,30 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
// type definition is wrong, should be "JSON" instead of "json"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
);
|
||||
const msg = ev.data as HassMessage;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
});
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -127,24 +126,26 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.playerState ===
|
||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface ReceivedMessage<T> {
|
||||
gj: boolean;
|
||||
data: T;
|
||||
senderId: string;
|
||||
type: "message";
|
||||
}
|
||||
@@ -40,6 +40,9 @@ const ENTITIES = [
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
getEntity("number", "number", 5, {
|
||||
friendly_name: "Number",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
@@ -377,6 +380,33 @@ const SCHEMAS: {
|
||||
name: "Constant",
|
||||
selector: { constant: { value: true, label: "Yes!" } },
|
||||
},
|
||||
choose: {
|
||||
name: "Choose",
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
number: {
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entity: {
|
||||
selector: {
|
||||
entity: {
|
||||
filter: {
|
||||
domain: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
32
package.json
32
package.json
@@ -37,15 +37,15 @@
|
||||
"@codemirror/view": "6.39.4",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||
"@formatjs/intl-displaynames": "6.8.13",
|
||||
"@formatjs/intl-durationformat": "0.7.6",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
||||
"@formatjs/intl-listformat": "7.7.13",
|
||||
"@formatjs/intl-locale": "4.2.13",
|
||||
"@formatjs/intl-numberformat": "8.15.6",
|
||||
"@formatjs/intl-pluralrules": "5.4.6",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.13",
|
||||
"@formatjs/intl-datetimeformat": "7.0.1",
|
||||
"@formatjs/intl-displaynames": "7.0.1",
|
||||
"@formatjs/intl-durationformat": "0.8.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.0.1",
|
||||
"@formatjs/intl-listformat": "8.0.1",
|
||||
"@formatjs/intl-locale": "5.0.1",
|
||||
"@formatjs/intl-numberformat": "9.0.2",
|
||||
"@formatjs/intl-pluralrules": "6.0.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.0.2",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
@@ -89,8 +89,6 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.9.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.6",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -114,7 +112,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"intl-messageformat": "11.0.1",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -157,11 +155,11 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.3.15",
|
||||
"@rsdoctor/rspack-plugin": "1.3.16",
|
||||
"@rspack/core": "1.6.7",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -178,7 +176,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -217,9 +215,9 @@
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.49.0",
|
||||
"typescript-eslint": "8.50.0",
|
||||
"vite-tsconfig-paths": "6.0.1",
|
||||
"vitest": "4.0.15",
|
||||
"vitest": "4.0.16",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -162,7 +162,7 @@ export class HaDevicePicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
|
||||
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
${item.domain
|
||||
? html`
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
interface AttributeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@@ -42,51 +37,44 @@ class HaEntityAttributePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _opened = false;
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
entityId: string | string[] | undefined,
|
||||
hideAttributes: string[] | undefined,
|
||||
hass: HomeAssistant
|
||||
): PickerComboBoxItem[] => {
|
||||
const entityIds = entityId ? ensureArray(entityId) : [];
|
||||
const options: PickerComboBoxItem[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(changedProps.has("_opened") && this._opened) ||
|
||||
changedProps.has("entityId") ||
|
||||
changedProps.has("attribute")
|
||||
) {
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
const entitiesOptions = entityIds.map<AttributeOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
for (const id of entityIds) {
|
||||
const stateObj = hass.states[id];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributes = Object.keys(stateObj.attributes).filter(
|
||||
(a) => !this.hideAttributes?.includes(a)
|
||||
(a) => !hideAttributes?.includes(a)
|
||||
);
|
||||
|
||||
return attributes.map((a) => ({
|
||||
value: a,
|
||||
label: this.hass.formatEntityAttributeName(stateObj, a),
|
||||
}));
|
||||
});
|
||||
|
||||
const options: AttributeOption[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
for (const entityOptions of entitiesOptions) {
|
||||
for (const option of entityOptions) {
|
||||
if (!optionsSet.has(option.value)) {
|
||||
optionsSet.add(option.value);
|
||||
options.push(option);
|
||||
for (const attribute of attributes) {
|
||||
if (!optionsSet.has(attribute)) {
|
||||
optionsSet.add(attribute);
|
||||
options.push({
|
||||
id: attribute,
|
||||
primary: hass.formatEntityAttributeName(stateObj, attribute),
|
||||
sorting_label: attribute,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(this._comboBox as any).filteredItems = options;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getItemsMemoized(this.entityId, this.hideAttributes, this.hass);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
@@ -94,10 +82,9 @@ class HaEntityAttributePicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label ??
|
||||
this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
@@ -106,38 +93,21 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -172,9 +172,9 @@ export class HaEntityPicker extends LitElement {
|
||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
index
|
||||
) => {
|
||||
const showEntityId = this._showEntityId;
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
@@ -20,21 +15,13 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
interface StateContentOption {
|
||||
primary: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
"access_token",
|
||||
"available_modes",
|
||||
@@ -111,63 +98,88 @@ export class HaStateContentPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@query(".container", true) private _container?: HTMLDivElement;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
private _options = memoizeOne(
|
||||
private _getItems = memoizeOne(
|
||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
return [
|
||||
const items: PickerComboBoxItem[] = [
|
||||
{
|
||||
id: "state",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.state"
|
||||
),
|
||||
value: "state",
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.state"
|
||||
),
|
||||
},
|
||||
...(allowName
|
||||
? [
|
||||
{
|
||||
id: "name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.name"
|
||||
),
|
||||
value: "name",
|
||||
},
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.name"
|
||||
),
|
||||
} satisfies PickerComboBoxItem,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "last_changed",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_changed"
|
||||
),
|
||||
value: "last_changed",
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_changed"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "last_updated",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
value: "last_updated",
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
},
|
||||
...(domain
|
||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||
).map((content) => ({
|
||||
primary: this.hass.localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
value: content,
|
||||
}))
|
||||
).map(
|
||||
(content) =>
|
||||
({
|
||||
id: content,
|
||||
primary: this.hass.localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
}) satisfies PickerComboBoxItem
|
||||
)
|
||||
: []),
|
||||
...Object.keys(stateObj?.attributes ?? {})
|
||||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
||||
.map((attribute) => ({
|
||||
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
|
||||
value: attribute,
|
||||
})),
|
||||
] satisfies StateContentOption[];
|
||||
.map(
|
||||
(attribute) =>
|
||||
({
|
||||
id: attribute,
|
||||
primary: this.hass.formatEntityAttributeName(
|
||||
stateObj!,
|
||||
attribute
|
||||
),
|
||||
sorting_label: this.hass.formatEntityAttributeName(
|
||||
stateObj!,
|
||||
attribute
|
||||
),
|
||||
}) satisfies PickerComboBoxItem
|
||||
),
|
||||
];
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -178,122 +190,123 @@ export class HaStateContentPicker extends LitElement {
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
|
||||
const options = this._options(this.entityId, stateObj, this.allowName);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="container ${this.disabled ? "disabled" : ""}">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item: string, idx) => {
|
||||
const label = options.find((o) => o.value === item)?.primary;
|
||||
const isValid = !!label;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label || item}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
|
||||
<mwc-menu-surface
|
||||
.open=${this._opened}
|
||||
@closed=${this._onClosed}
|
||||
@opened=${this._onOpened}
|
||||
@input=${stopPropagation}
|
||||
.anchor=${this._container}
|
||||
>
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.value=${""}
|
||||
.autofocus=${this.autofocus}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required && !value.length}
|
||||
.helper=${this.helper}
|
||||
.items=${options}
|
||||
allow-custom-value
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="primary"
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._getPickerValue()}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
</ha-combo-box>
|
||||
</mwc-menu-surface>
|
||||
</div>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item: string, idx) => {
|
||||
const label = this._getItemLabel(item, stateObj);
|
||||
const isValid = !!label;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label || item}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _onClosed(ev) {
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._opened = false;
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _onOpened(ev) {
|
||||
if (!this._opened) {
|
||||
return;
|
||||
}
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
await this._comboBox?.focus();
|
||||
await this._comboBox?.open();
|
||||
}
|
||||
|
||||
private async _addItem(ev) {
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private async _editItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return !this.value ? [] : ensureArray(this.value);
|
||||
}
|
||||
|
||||
private _getItemLabel = memoizeOne(
|
||||
(value: string, stateObj?: HassEntity): string | undefined => {
|
||||
const stateObjForItems = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: stateObj;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObjForItems,
|
||||
this.allowName
|
||||
);
|
||||
return items.find((item) => item.id === value)?.primary;
|
||||
}
|
||||
);
|
||||
|
||||
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
||||
if (value.length === 0) {
|
||||
return undefined;
|
||||
@@ -304,63 +317,87 @@ export class HaStateContentPicker extends LitElement {
|
||||
return value;
|
||||
});
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const open = ev.detail.value;
|
||||
if (open) {
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const initialValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
this._comboBox.setInputValue(initialValue);
|
||||
} else {
|
||||
this._opened = false;
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
return this._value[this._editIndex];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _filterSelectedOptions = (
|
||||
options: StateContentOption[],
|
||||
current?: string
|
||||
) => {
|
||||
private _customValueOption = memoizeOne(
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
id: text,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
),
|
||||
secondary: `"${text}"`,
|
||||
search_labels: {
|
||||
primary: text,
|
||||
secondary: `"${text}"`,
|
||||
id: text,
|
||||
},
|
||||
sorting_label: text,
|
||||
})
|
||||
);
|
||||
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
const value = this._value;
|
||||
|
||||
return options.filter(
|
||||
(option) => !value.includes(option.value) || option.value === current
|
||||
);
|
||||
};
|
||||
|
||||
private _filterChanged(ev: ValueChangedEvent<string>) {
|
||||
const input = ev.detail.value;
|
||||
const filter = input?.toLowerCase() || "";
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||
|
||||
this._comboBox.filteredItems = this._filterSelectedOptions(
|
||||
options,
|
||||
currentValue
|
||||
const filteredItems = items.filter(
|
||||
(item) => !value.includes(item.id) || item.id === currentValue
|
||||
);
|
||||
|
||||
if (!filter) {
|
||||
return;
|
||||
// When editing an existing custom value, include it in the base items
|
||||
if (
|
||||
currentValue &&
|
||||
!items.find((item) => item.id === currentValue) &&
|
||||
!searchString
|
||||
) {
|
||||
filteredItems.push(this._customValueOption(currentValue));
|
||||
}
|
||||
|
||||
const fuseOptions: IFuseOptions<StateContentOption> = {
|
||||
keys: ["primary", "secondary", "value"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
|
||||
const filteredItems = fuse.search(filter).map((result) => result.item);
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
// Don't add if it's the same as the current item being edited
|
||||
if (currentValue && currentValue === searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the search string matches an existing item
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
|
||||
// Only return custom value option if it doesn't match an existing item
|
||||
if (!existingItem) {
|
||||
return [this._customValueOption(searchString)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -370,25 +407,21 @@ export class HaStateContentPicker extends LitElement {
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private async _removeItem(ev) {
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._value];
|
||||
const idx = parseInt(ev.target.dataset.idx, 10);
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || value === "") {
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -396,11 +429,16 @@ export class HaStateContentPicker extends LitElement {
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = value;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(value);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string[]) {
|
||||
@@ -442,7 +480,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
.container.disabled:after {
|
||||
:host([disabled]) .container:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
@@ -462,10 +500,6 @@ export class HaStateContentPicker extends LitElement {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
mwc-menu-surface {
|
||||
--mdc-menu-min-width: 100%;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
@@ -486,6 +520,11 @@ export class HaStateContentPicker extends LitElement {
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { getStates } from "../../common/entity/get_states";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
interface StateOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-entity-state-picker")
|
||||
class HaEntityStatePicker extends LitElement {
|
||||
export class HaEntityStatePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string | string[];
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@property({ attribute: false }) public extraOptions?: any[];
|
||||
@property({ attribute: false }) public extraOptions?: PickerComboBoxItem[];
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
@@ -42,59 +37,76 @@ class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _opened = false;
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
entityId: string | string[] | undefined,
|
||||
attribute: string | undefined,
|
||||
hideStates: string[] | undefined,
|
||||
extraOptions: PickerComboBoxItem[] | undefined
|
||||
): PickerComboBoxItem[] => {
|
||||
const entityIds = entityId ? ensureArray(entityId) : [];
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
const entitiesOptions = entityIds.map<PickerComboBoxItem[]>(
|
||||
(entityIdItem) => {
|
||||
const stateObj = hass.states[entityIdItem] || {
|
||||
entity_id: entityIdItem,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
const states = getStates(hass, stateObj, attribute).filter(
|
||||
(s) => !hideStates?.includes(s)
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(changedProps.has("_opened") && this._opened) ||
|
||||
changedProps.has("entityId") ||
|
||||
changedProps.has("attribute") ||
|
||||
changedProps.has("extraOptions")
|
||||
) {
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
return states
|
||||
.map((s) => {
|
||||
const primary = attribute
|
||||
? hass.formatEntityAttributeValue(stateObj, attribute, s)
|
||||
: hass.formatEntityState(stateObj, s);
|
||||
return {
|
||||
id: s,
|
||||
primary,
|
||||
sorting_label: primary,
|
||||
};
|
||||
})
|
||||
.filter((option) => option.id && option.primary);
|
||||
}
|
||||
);
|
||||
|
||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId] || {
|
||||
entity_id: entityId,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||
(s) => !this.hideStates?.includes(s)
|
||||
);
|
||||
|
||||
return states.map((s) => ({
|
||||
value: s,
|
||||
label: this.attribute
|
||||
? this.hass.formatEntityAttributeValue(stateObj, this.attribute, s)
|
||||
: this.hass.formatEntityState(stateObj, s),
|
||||
}));
|
||||
});
|
||||
|
||||
const options: StateOption[] = [];
|
||||
const options: PickerComboBoxItem[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
for (const entityOptions of entitiesOptions) {
|
||||
for (const option of entityOptions) {
|
||||
if (!optionsSet.has(option.value)) {
|
||||
optionsSet.add(option.value);
|
||||
if (!optionsSet.has(option.id)) {
|
||||
optionsSet.add(option.id);
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extraOptions) {
|
||||
options.unshift(...this.extraOptions);
|
||||
if (extraOptions) {
|
||||
// Filter out any extraOptions with empty primary or id fields
|
||||
const validExtraOptions = extraOptions.filter(
|
||||
(option) => option.id && option.primary
|
||||
);
|
||||
options.unshift(...validExtraOptions);
|
||||
}
|
||||
|
||||
(this._comboBox as any).filteredItems = options;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
private _getFilteredItems = (
|
||||
_searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] =>
|
||||
this._getItems(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
this.attribute,
|
||||
this.hideStates,
|
||||
this.extraOptions
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
@@ -102,48 +114,39 @@ class HaEntityStatePicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.autofocus=${this.autofocus}
|
||||
.required=${this.required}
|
||||
.label=${this.label ??
|
||||
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.add_custom_state"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue !== this._value) {
|
||||
if (newValue !== this.value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
private _setValue(value: string | undefined) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
@@ -424,9 +424,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
|
||||
private _rowRenderer: RenderItemFunction<StatisticComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
index
|
||||
) => {
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
return html`
|
||||
|
||||
@@ -175,7 +175,7 @@ export class HaAutomationRow extends LitElement {
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
margin: var(--ha-space-0) var(--ha-space-3);
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
|
||||
@@ -241,7 +241,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
slot[name="footer"] {
|
||||
display: block;
|
||||
padding: var(--ha-space-0);
|
||||
padding: 0;
|
||||
}
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
|
||||
@@ -46,14 +46,14 @@ export class HaCard extends LitElement {
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
|
||||
display: block;
|
||||
margin-block-start: var(--ha-space-0);
|
||||
margin-block-end: var(--ha-space-0);
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
slot:not(:first-child)::slotted(.card-content) {
|
||||
padding-top: var(--ha-space-0);
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { comboBoxRenderer } from "@vaadin/combo-box/lit";
|
||||
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
|
||||
import type {
|
||||
ComboBoxDataProvider,
|
||||
ComboBoxLight,
|
||||
ComboBoxLightFilterChangedEvent,
|
||||
ComboBoxLightOpenedChangedEvent,
|
||||
ComboBoxLightValueChangedEvent,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-combo-box-textfield";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
registerStyles(
|
||||
"vaadin-combo-box-item",
|
||||
css`
|
||||
:host {
|
||||
padding: 0 !important;
|
||||
}
|
||||
:host([focused]:not([disabled])) {
|
||||
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
|
||||
}
|
||||
:host([selected]:not([disabled])) {
|
||||
background-color: transparent;
|
||||
color: var(--mdc-theme-primary);
|
||||
--mdc-ripple-color: var(--mdc-theme-primary);
|
||||
--mdc-theme-text-primary-on-background: var(--mdc-theme-primary);
|
||||
}
|
||||
:host([selected]:not([disabled])):before {
|
||||
background-color: var(--mdc-theme-primary);
|
||||
opacity: 0.12;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:host([selected][focused]:not([disabled])):before {
|
||||
opacity: 0.24;
|
||||
}
|
||||
:host(:hover:not([disabled])) {
|
||||
background-color: transparent;
|
||||
}
|
||||
[part="content"] {
|
||||
width: 100%;
|
||||
}
|
||||
[part="checkmark"] {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
@customElement("ha-combo-box")
|
||||
export class HaComboBox extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ attribute: false }) public validationMessage?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ attribute: false }) public items?: any[];
|
||||
|
||||
@property({ attribute: false }) public filteredItems?: any[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public dataProvider?: ComboBoxDataProvider<any>;
|
||||
|
||||
@property({ attribute: "allow-custom-value", type: Boolean })
|
||||
public allowCustomValue = false;
|
||||
|
||||
@property({ attribute: "item-value-path" }) public itemValuePath = "value";
|
||||
|
||||
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
||||
|
||||
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
||||
|
||||
@property({ attribute: false }) public renderer?: ComboBoxLitRenderer<any>;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public opened = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "clear-initial-value" })
|
||||
public clearInitialValue = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||
|
||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
||||
|
||||
@state({ type: Boolean }) private _forceBlankValue = false;
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
private _bodyMutationObserver?: MutationObserver;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
this._comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this._inputElement?.updateComplete;
|
||||
this._inputElement?.focus();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._overlayMutationObserver) {
|
||||
this._overlayMutationObserver.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
if (this._bodyMutationObserver) {
|
||||
this._bodyMutationObserver.disconnect();
|
||||
this._bodyMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get selectedItem() {
|
||||
return this._comboBox.selectedItem;
|
||||
}
|
||||
|
||||
public setInputValue(value: string) {
|
||||
this._comboBox.value = value;
|
||||
}
|
||||
|
||||
public setTextFieldValue(value: string) {
|
||||
this._inputElement.value = value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
||||
<vaadin-combo-box-light
|
||||
.itemValuePath=${this.itemValuePath}
|
||||
.itemIdPath=${this.itemIdPath}
|
||||
.itemLabelPath=${this.itemLabelPath}
|
||||
.items=${this.items}
|
||||
.value=${this.value || ""}
|
||||
.filteredItems=${this.filteredItems}
|
||||
.dataProvider=${this.dataProvider}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
attr-for-value="value"
|
||||
>
|
||||
<ha-combo-box-textfield
|
||||
label=${ifDefined(this.label)}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
validationMessage=${ifDefined(this.validationMessage)}
|
||||
.errorMessage=${this.errorMessage}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
.autocorrect=${false}
|
||||
input-spellcheck="false"
|
||||
.suffix=${html`<div
|
||||
style="width: 28px;"
|
||||
role="none presentation"
|
||||
></div>`}
|
||||
.icon=${this.icon}
|
||||
.invalid=${this.invalid}
|
||||
.forceBlankValue=${this._forceBlankValue}
|
||||
>
|
||||
<slot name="icon" slot="leadingIcon"></slot>
|
||||
</ha-combo-box-textfield>
|
||||
${this.value && !this.hideClearIcon
|
||||
? html`<ha-svg-icon
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
||||
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${mdiClose}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._clearValue}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
<ha-svg-icon
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-expanded=${this.opened ? "true" : "false"}
|
||||
class=${`toggle-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._toggleOpen}
|
||||
></ha-svg-icon>
|
||||
</vaadin-combo-box-light>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: "";
|
||||
}
|
||||
|
||||
private _defaultRowRenderer: ComboBoxLitRenderer<
|
||||
string | Record<string, any>
|
||||
> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
${this.itemLabelPath ? item[this.itemLabelPath] : item}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
}
|
||||
|
||||
private _toggleOpen(ev: Event) {
|
||||
if (this.opened) {
|
||||
this._comboBox?.close();
|
||||
ev.stopPropagation();
|
||||
} else {
|
||||
this._comboBox?.inputElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
const opened = ev.detail.value;
|
||||
// delay this so we can handle click event for toggle button before setting _opened
|
||||
setTimeout(() => {
|
||||
this.opened = opened;
|
||||
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
||||
}, 0);
|
||||
|
||||
if (this.clearInitialValue) {
|
||||
this.setTextFieldValue("");
|
||||
if (opened) {
|
||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
||||
setTimeout(() => {
|
||||
this._forceBlankValue = false;
|
||||
}, 100);
|
||||
} else {
|
||||
this._forceBlankValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (opened) {
|
||||
const overlay = document.querySelector<HTMLElement>(
|
||||
"vaadin-combo-box-overlay"
|
||||
);
|
||||
|
||||
if (overlay) {
|
||||
this._removeInert(overlay);
|
||||
}
|
||||
this._observeBody();
|
||||
} else {
|
||||
this._bodyMutationObserver?.disconnect();
|
||||
this._bodyMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _observeBody() {
|
||||
if ("MutationObserver" in window && !this._bodyMutationObserver) {
|
||||
this._bodyMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._removeInert(node as HTMLElement);
|
||||
}
|
||||
});
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._bodyMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _removeInert(overlay: HTMLElement) {
|
||||
if (overlay.inert) {
|
||||
overlay.inert = false;
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
return;
|
||||
}
|
||||
if ("MutationObserver" in window && !this._overlayMutationObserver) {
|
||||
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === "inert") {
|
||||
const target = mutation.target as HTMLElement;
|
||||
if (target.inert) {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
target.inert = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._overlayMutationObserver.observe(overlay, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "filter-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this.allowCustomValue) {
|
||||
// @ts-ignore
|
||||
this._comboBox._closeOnBlurIsPrevented = true;
|
||||
}
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue !== this.value) {
|
||||
fireEvent(this, "value-changed", { value: newValue || undefined });
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
vaadin-combo-box-light {
|
||||
position: relative;
|
||||
}
|
||||
ha-combo-box-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
ha-combo-box-textfield > ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--input-dropdown-icon-color);
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-button {
|
||||
right: 12px;
|
||||
top: -10px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.toggle-button[disabled],
|
||||
.clear-button[disabled] {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.toggle-button.no-label {
|
||||
top: -3px;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
right: 36px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 36px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.clear-button.no-label {
|
||||
top: 0;
|
||||
}
|
||||
ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-combo-box": HaComboBox;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"filter-changed": { value: string };
|
||||
"opened-changed": { value: boolean };
|
||||
}
|
||||
}
|
||||
@@ -101,14 +101,13 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding: var(--dialog-container-padding, var(--ha-space-0));
|
||||
padding: var(--dialog-container-padding, 0);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
|
||||
var(--ha-space-4);
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0 var(--ha-space-4);
|
||||
}
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: var(--ha-space-3) var(--ha-space-3) var(--ha-space-0);
|
||||
padding: var(--ha-space-3) var(--ha-space-3) 0;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
content: unset;
|
||||
@@ -136,7 +135,7 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
padding: var(--dialog-surface-padding, var(--ha-space-0));
|
||||
padding: var(--dialog-surface-padding, 0);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
@@ -168,7 +167,7 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: var(--ha-space-0) !important;
|
||||
inset-inline-end: 0 !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -303,7 +303,7 @@ export class HaFloorPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (item) => html`
|
||||
private _rowRenderer: RenderItemFunction<FloorComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${item.icon_path
|
||||
? html`
|
||||
|
||||
@@ -393,7 +393,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
--wa-space-l: 0;
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
@@ -418,7 +418,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
--ha-bottom-sheet-max-width: 600px;
|
||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||
--ha-bottom-sheet-padding: 0;
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static styles = css`
|
||||
ha-chip-set {
|
||||
margin-bottom: 8px;
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
|
||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
margin-top: var(--safe-area-inset-top, 0);
|
||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
||||
margin-left: var(--safe-area-inset-left, 0);
|
||||
margin-right: var(--safe-area-inset-right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
202
src/components/ha-selector/ha-selector-choose.ts
Normal file
202
src/components/ha-selector/ha-selector-choose.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isTemplate } from "../../common/string/has-template";
|
||||
import type { ChooseSelector, Selector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
import "./ha-selector";
|
||||
|
||||
@customElement("ha-selector-choose")
|
||||
export class HaChooseSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ChooseSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public localizeValue?: (key: string) => string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() public _activeChoice?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
(!this._activeChoice ||
|
||||
!(this._activeChoice in this.selector.choose.choices))
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._activeChoice) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const selector = this._selector(this._activeChoice);
|
||||
const value = this._value(this._activeChoice);
|
||||
|
||||
return html`<div class="multi-header">
|
||||
<span>${this.label}${this.required ? "*" : ""}</span>
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.value=${value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
.helper=${this.helper}
|
||||
></ha-selector>`;
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne(
|
||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
this.localizeValue && translationKey
|
||||
? this.localizeValue(`${translationKey}.choices.${choice}`)
|
||||
: choice,
|
||||
value: choice,
|
||||
}))
|
||||
);
|
||||
|
||||
private _choiceChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value =
|
||||
typeof this.value === "object"
|
||||
? this.value
|
||||
: {
|
||||
[this._activeChoice!]: this.value,
|
||||
};
|
||||
this._activeChoice = ev.detail?.value || ev.target.value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...value,
|
||||
active_choice: this._activeChoice,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = typeof this.value === "object" ? this.value : {};
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...value,
|
||||
[this._activeChoice!]: ev.detail.value,
|
||||
active_choice: this._activeChoice,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _selector(choice?: string): Selector {
|
||||
const choices = this.selector.choose.choices;
|
||||
|
||||
choice = choice || this.value?.active_choice;
|
||||
|
||||
if (choice && choice in choices) {
|
||||
return choices[choice].selector;
|
||||
}
|
||||
|
||||
return choices[Object.keys(choices)[0]].selector;
|
||||
}
|
||||
|
||||
private _value(choice?: string): any {
|
||||
if (!this.value) {
|
||||
return undefined;
|
||||
}
|
||||
return typeof this.value === "object"
|
||||
? this.value[choice || this.value.active_choice]
|
||||
: this.value;
|
||||
}
|
||||
|
||||
private _setActiveChoice() {
|
||||
if (this.value) {
|
||||
if (typeof this.value === "object") {
|
||||
if (this.value.active_choice in this.selector.choose.choices) {
|
||||
this._activeChoice = this.value.active_choice;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const typeofValue = typeof this.value;
|
||||
const selectorTypes = Object.values(this.selector.choose.choices).map(
|
||||
(choice) => Object.keys(choice.selector)[0]
|
||||
);
|
||||
if (typeofValue === "number" && selectorTypes.includes("number")) {
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
||||
selectorTypes.indexOf("number")
|
||||
];
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeofValue === "string" &&
|
||||
isTemplate(this.value) &&
|
||||
selectorTypes.includes("template")
|
||||
) {
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
||||
selectorTypes.indexOf("template")
|
||||
];
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeofValue === "string" &&
|
||||
this.value.includes(".") &&
|
||||
selectorTypes.includes("entity")
|
||||
) {
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
||||
selectorTypes.indexOf("entity")
|
||||
];
|
||||
return;
|
||||
}
|
||||
if (typeofValue === "string" && selectorTypes.includes("text")) {
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
||||
selectorTypes.indexOf("text")
|
||||
];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[0];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.multi-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
display: block;
|
||||
justify-self: end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-choose": HaChooseSelector;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
@@ -11,9 +12,8 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-formfield";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-list-item";
|
||||
import "../ha-radio";
|
||||
@@ -40,8 +40,6 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -59,15 +57,8 @@ export class HaSelectSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _filter = "";
|
||||
|
||||
protected render() {
|
||||
const options =
|
||||
this.selector.select?.options?.map((option) =>
|
||||
typeof option === "object"
|
||||
? (option as SelectOption)
|
||||
: ({ value: option, label: option } as SelectOption)
|
||||
) || [];
|
||||
const options = this._getOptions(this.selector);
|
||||
|
||||
const translationKey = this.selector.select?.translation_key;
|
||||
|
||||
@@ -165,10 +156,6 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
|
||||
const optionItems = options.filter(
|
||||
(option) => !option.disabled && !value?.includes(option.value)
|
||||
);
|
||||
|
||||
return html`
|
||||
${value?.length
|
||||
? html`
|
||||
@@ -212,50 +199,33 @@ export class HaSelectSelector extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${""}
|
||||
.items=${optionItems}
|
||||
.addButtonLabel=${this.label}
|
||||
.getItems=${this._getItems(options, value, true)}
|
||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
></ha-combo-box>
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.selector.select?.custom_value) {
|
||||
if (
|
||||
this.value !== undefined &&
|
||||
!Array.isArray(this.value) &&
|
||||
!options.find((option) => option.value === this.value)
|
||||
) {
|
||||
options.unshift({ value: this.value, label: this.value });
|
||||
}
|
||||
|
||||
const optionItems = options.filter((option) => !option.disabled);
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.items=${optionItems}
|
||||
.value=${this.value}
|
||||
@filter-changed=${this._filterChanged}
|
||||
.getItems=${this._getItems(options)}
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
></ha-combo-box>
|
||||
allow-custom-value
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -274,7 +244,7 @@ export class HaSelectSelector extends LitElement {
|
||||
>
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-list-item .value=${item.value} .disabled=${item.disabled}
|
||||
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
|
||||
>${item.label}</ha-list-item
|
||||
>
|
||||
`
|
||||
@@ -291,6 +261,30 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _getOptions = memoizeOne(
|
||||
(selector: SelectSelector) =>
|
||||
selector.select?.options?.map((option) =>
|
||||
typeof option === "object"
|
||||
? (option as SelectOption)
|
||||
: ({ value: option, label: option } as SelectOption)
|
||||
) || []
|
||||
);
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(options: SelectOption[], value?: string[], multiple = false) => {
|
||||
const filteredOptions = options.filter((option) =>
|
||||
!option.disabled && !multiple ? true : !value?.includes(option.value)
|
||||
);
|
||||
|
||||
return () =>
|
||||
filteredOptions.map((option) => ({
|
||||
id: option.value,
|
||||
primary: option.label,
|
||||
sorting_label: option.label,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private get _mode(): "list" | "dropdown" | "box" {
|
||||
return (
|
||||
this.selector.select?.mode ||
|
||||
@@ -355,8 +349,6 @@ export class HaSelectSelector extends LitElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
await this.updateComplete;
|
||||
this._filterChanged();
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: CustomEvent): void {
|
||||
@@ -374,49 +366,17 @@ export class HaSelectSelector extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
const currentValue = !this.value ? [] : ensureArray(this.value);
|
||||
|
||||
if (newValue !== undefined && currentValue.includes(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._filterChanged();
|
||||
this.comboBox.setInputValue("");
|
||||
}, 0);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: [...currentValue, newValue],
|
||||
});
|
||||
}
|
||||
|
||||
private _openedChanged(ev?: CustomEvent): void {
|
||||
if (ev?.detail.value) {
|
||||
this._filterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _filterChanged(ev?: CustomEvent): void {
|
||||
this._filter = ev?.detail.value || "";
|
||||
|
||||
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||
const label = item.label || item.value;
|
||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||
});
|
||||
|
||||
if (
|
||||
this._filter &&
|
||||
this.selector.select?.custom_value &&
|
||||
filteredItems &&
|
||||
!filteredItems.some((item) => (item.label || item.value) === this._filter)
|
||||
) {
|
||||
filteredItems.unshift({ label: this._filter, value: this._filter });
|
||||
}
|
||||
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
|
||||
@@ -18,6 +18,7 @@ const LOAD_ELEMENTS = {
|
||||
attribute: () => import("./ha-selector-attribute"),
|
||||
assist_pipeline: () => import("./ha-selector-assist-pipeline"),
|
||||
boolean: () => import("./ha-selector-boolean"),
|
||||
choose: () => import("./ha-selector-choose"),
|
||||
color_rgb: () => import("./ha-selector-color-rgb"),
|
||||
condition: () => import("./ha-selector-condition"),
|
||||
config_entry: () => import("./ha-selector-config-entry"),
|
||||
|
||||
@@ -726,6 +726,7 @@ export class HaServiceControl extends LitElement {
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.required=${dataField.required}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiRoomService } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -56,9 +56,9 @@ class HaServicePicker extends LitElement {
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<ServiceComboBoxItem> = (
|
||||
private _rowRenderer: RenderItemFunction<ServiceComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
index
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
<ha-service-icon
|
||||
|
||||
@@ -681,7 +681,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.position = "fixed";
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.style.left = `calc(${item.offsetLeft + item.clientWidth + 8}px + var(--safe-area-inset-left, var(--ha-space-0)))`;
|
||||
tooltip.style.left = `calc(${item.offsetLeft + item.clientWidth + 8}px + var(--safe-area-inset-left, 0px))`;
|
||||
}
|
||||
|
||||
private _hideTooltip() {
|
||||
@@ -720,14 +720,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(
|
||||
14px + var(--safe-area-inset-bottom, var(--ha-space-0))
|
||||
);
|
||||
padding-bottom: calc(14px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
.menu {
|
||||
height: calc(
|
||||
var(--header-height) + var(--safe-area-inset-top, var(--ha-space-0))
|
||||
);
|
||||
height: calc(var(--header-height) + var(--safe-area-inset-top, 0px));
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 0 var(--ha-space-1);
|
||||
@@ -746,16 +742,16 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
align-items: center;
|
||||
padding-left: calc(
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, var(--ha-space-0))
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
padding-inline-start: calc(
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, var(--ha-space-0))
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
padding-inline-end: initial;
|
||||
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
padding-top: var(--safe-area-inset-top, 0px);
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + var(--safe-area-inset-left, var(--ha-space-0)));
|
||||
width: calc(256px + var(--safe-area-inset-left, 0px));
|
||||
}
|
||||
:host([narrow][expanded]) .menu {
|
||||
width: 100%;
|
||||
@@ -771,8 +767,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
display: none;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: var(--ha-space-0);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
@@ -784,16 +780,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
ha-fade-in,
|
||||
ha-md-list {
|
||||
height: calc(
|
||||
100% - var(--header-height) - var(
|
||||
--safe-area-inset-top,
|
||||
var(--ha-space-0)
|
||||
) -
|
||||
100% - var(--header-height) - var(--safe-area-inset-top, 0px) -
|
||||
132px
|
||||
);
|
||||
}
|
||||
|
||||
ha-fade-in {
|
||||
padding: var(--ha-space-1) var(--ha-space-0);
|
||||
padding: var(--ha-space-1) 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -803,7 +796,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
ha-md-list {
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
margin-left: var(--safe-area-inset-left, 0px);
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
@@ -825,7 +818,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
width: 248px;
|
||||
}
|
||||
:host([narrow][expanded]) ha-md-list-item {
|
||||
width: calc(240px - var(--safe-area-inset-left, var(--ha-space-0)));
|
||||
width: calc(240px - var(--safe-area-inset-left, 0px));
|
||||
}
|
||||
|
||||
ha-md-list-item.selected {
|
||||
@@ -898,7 +891,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
font-size: 0.65em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: var(--ha-space-0) var(--ha-space-1);
|
||||
padding: 0 var(--ha-space-1);
|
||||
}
|
||||
|
||||
ha-md-list-item.user {
|
||||
|
||||
@@ -988,7 +988,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
style=${styleMap({
|
||||
width: "var(--ha-space-12)",
|
||||
position: "absolute",
|
||||
top: "var(--ha-space-0)",
|
||||
top: "0",
|
||||
left: rtl ? undefined : "var(--ha-space-1)",
|
||||
right: rtl ? "var(--ha-space-1)" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
@@ -1092,7 +1092,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
z-index: 2;
|
||||
}
|
||||
.mdc-chip-set {
|
||||
padding: var(--ha-space-1) var(--ha-space-0);
|
||||
padding: var(--ha-space-1) 0;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
|
||||
@@ -266,15 +266,15 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
/* Used to offset the dialog from the safe areas when space is limited */
|
||||
transform: translate(
|
||||
calc(
|
||||
var(--safe-area-offset-left, var(--ha-space-0)) - var(
|
||||
var(--safe-area-offset-left, 0px) - var(
|
||||
--safe-area-offset-right,
|
||||
var(--ha-space-0)
|
||||
0px
|
||||
)
|
||||
),
|
||||
calc(
|
||||
var(--safe-area-offset-top, var(--ha-space-0)) - var(
|
||||
var(--safe-area-offset-top, 0px) - var(
|
||||
--safe-area-offset-bottom,
|
||||
var(--ha-space-0)
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -285,7 +285,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: var(--ha-space-0);
|
||||
--ha-dialog-border-radius: 0;
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
@@ -372,7 +372,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-dialog::part(footer) {
|
||||
padding: var(--ha-space-0);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
--expansion-panel-content-padding: var(--ha-space-0);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
ha-expansion-panel::part(summary) {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
@@ -101,7 +101,7 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
min-height: unset;
|
||||
}
|
||||
ha-md-list {
|
||||
padding: var(--ha-space-0);
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -605,8 +605,8 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
:host {
|
||||
--md-list-item-top-space: var(--ha-space-0);
|
||||
--md-list-item-bottom-space: var(--ha-space-0);
|
||||
--md-list-item-top-space: 0;
|
||||
--md-list-item-bottom-space: 0;
|
||||
--md-list-item-leading-space: var(--ha-space-2);
|
||||
--md-list-item-trailing-space: var(--ha-space-2);
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
|
||||
@@ -278,8 +278,8 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
direction: var(--direction);
|
||||
}
|
||||
.expand-btn {
|
||||
margin-right: var(--ha-space-0);
|
||||
margin-inline-end: var(--ha-space-0);
|
||||
margin-right: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.mdc-chip.area:not(.add),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -73,7 +73,7 @@ class HaUserPicker extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<UserComboBoxItem> = (item) => {
|
||||
private _rowRenderer: RenderItemFunction<UserComboBoxItem> = (item) => {
|
||||
const user = item.user;
|
||||
if (!user) {
|
||||
return html`<ha-combo-box-item type="button" compact>
|
||||
|
||||
@@ -27,6 +27,7 @@ export type Selector =
|
||||
| AttributeSelector
|
||||
| BooleanSelector
|
||||
| ButtonToggleSelector
|
||||
| ChooseSelector
|
||||
| ColorRGBSelector
|
||||
| ColorTempSelector
|
||||
| ConditionSelector
|
||||
@@ -116,6 +117,13 @@ export interface ButtonToggleSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ChooseSelector {
|
||||
choose: {
|
||||
choices: Record<string, { selector: Selector }>;
|
||||
translation_key?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColorRGBSelector {
|
||||
color_rgb: {} | null;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ export class HaMoreInfoControlSelectContainer extends LitElement {
|
||||
overflow: auto;
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
margin: -2px -24px;
|
||||
padding: 2px 24px;
|
||||
margin: -2px calc(var(--ha-space-6) * -1);
|
||||
padding: 2px var(--ha-space-6);
|
||||
}
|
||||
.controls-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
@@ -84,8 +84,8 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
letter-spacing: 0.1px;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 20px;
|
||||
padding: var(--ha-space-1) 0;
|
||||
margin-bottom: var(--ha-space-5);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -268,14 +268,14 @@ class DialogLightColorFavorite extends LitElement {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
padding: var(--ha-space-6);
|
||||
flex: 1;
|
||||
}
|
||||
.modes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 0 24px;
|
||||
padding: 0 var(--ha-space-6);
|
||||
}
|
||||
.wheel {
|
||||
width: 30px;
|
||||
|
||||
@@ -254,14 +254,14 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: -8px;
|
||||
margin: calc(var(--ha-space-2) * -1);
|
||||
flex-wrap: wrap;
|
||||
max-width: 250px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
margin: 8px;
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.color {
|
||||
|
||||
@@ -515,7 +515,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -15,22 +15,22 @@ export const moreInfoControlStyle = css`
|
||||
}
|
||||
|
||||
.controls:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.controls > *:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.buttons > * {
|
||||
margin: 8px;
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-attributes {
|
||||
@@ -38,6 +38,6 @@ export const moreInfoControlStyle = css`
|
||||
width: 100%;
|
||||
}
|
||||
ha-more-info-control-select-container + ha-attributes:not([empty]) {
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -209,7 +209,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-4);
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-border-radius: var(--ha-border-radius-xl);
|
||||
|
||||
@@ -48,7 +48,7 @@ class MoreInfoViewVoiceAssistants extends LitElement {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
padding: var(--ha-space-6);
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -52,7 +52,7 @@ class MoreInfoAutomation extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@@ -60,7 +60,7 @@ class MoreInfoAutomation extends LitElement {
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class MoreInfoCamera extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
z-index: 1;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@@ -511,7 +511,7 @@ class MoreInfoClimate extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: var(--ha-space-10);
|
||||
}
|
||||
|
||||
.current div {
|
||||
@@ -534,7 +534,7 @@ class MoreInfoClimate extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.4px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.current .value {
|
||||
@@ -545,7 +545,7 @@ class MoreInfoClimate extends LitElement {
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.container-humidity .single-row {
|
||||
@@ -561,7 +561,7 @@ class MoreInfoClimate extends LitElement {
|
||||
}
|
||||
|
||||
.single-row {
|
||||
padding: 8px 0;
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -87,7 +87,7 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -62,7 +62,7 @@ class MoreInfoCounter extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.actions {
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
@@ -194,7 +194,7 @@ class MoreInfoCover extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
.main-control > * {
|
||||
margin: 0 8px;
|
||||
margin: 0 var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -72,8 +72,8 @@ class MoreInfoDatetime extends LitElement {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-date-input + ha-time-input {
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-left: var(--ha-space-1);
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -100,7 +100,7 @@ class MoreInfoGroup extends LitElement {
|
||||
css`
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -217,7 +217,7 @@ class MoreInfoHumidifier extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: var(--ha-space-10);
|
||||
}
|
||||
.current div {
|
||||
display: flex;
|
||||
@@ -237,7 +237,7 @@ class MoreInfoHumidifier extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.4px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
.current .value {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
|
||||
@@ -84,8 +84,8 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-date-input + ha-time-input {
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-left: var(--ha-space-1);
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -192,7 +192,7 @@ class MoreInfoLock extends LitElement {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-control-button-group + ha-attributes:not([empty]) {
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
|
||||
@@ -528,11 +528,11 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
margin-left: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.volume ha-svg-icon {
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
@@ -545,17 +545,17 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 16px;
|
||||
left: var(--ha-space-4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
min-width: 8px;
|
||||
min-width: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
background-color: var(--primary-color);
|
||||
padding: 0 4px;
|
||||
padding: 0 var(--ha-space-1);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
@@ -572,13 +572,13 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0 8px 8px;
|
||||
margin: var(--ha-space-2) 0 var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.media-artist {
|
||||
|
||||
@@ -72,12 +72,12 @@ class MoreInfoPerson extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
text-align: right;
|
||||
}
|
||||
ha-map {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -227,24 +227,24 @@ class MoreInfoScript extends LitElement {
|
||||
visibility: hidden;
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
height: 21px;
|
||||
}
|
||||
.queue.has-queue {
|
||||
visibility: visible;
|
||||
}
|
||||
.fields {
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.fields .title {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
ha-control-button ha-svg-icon {
|
||||
z-index: -1;
|
||||
margin-right: 4px;
|
||||
margin-right: var(--ha-space-1);
|
||||
}
|
||||
ha-service-control {
|
||||
--service-control-padding: 0;
|
||||
@@ -252,7 +252,7 @@ class MoreInfoScript extends LitElement {
|
||||
}
|
||||
ha-markdown {
|
||||
text-align: center;
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class MoreInfoSun extends LitElement {
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class MoreInfoTimer extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.actions {
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
@@ -448,14 +448,14 @@ class MoreInfoUpdate extends LitElement {
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -473,8 +473,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin: 0 -24px 0 -24px;
|
||||
margin-bottom: calc(-1 * max(var(--safe-area-inset-bottom), 24px));
|
||||
margin: 0 calc(var(--ha-space-6) * -1) 0 calc(var(--ha-space-6) * -1);
|
||||
margin-bottom: calc(
|
||||
-1 * max(var(--safe-area-inset-bottom), var(--ha-space-6))
|
||||
);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -486,8 +488,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: -16px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: calc(var(--ha-space-4) * -1);
|
||||
margin-top: calc(var(--ha-space-1) * -1);
|
||||
--md-sys-color-surface: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
@@ -495,8 +497,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
--md-list-item-leading-space: var(--ha-space-6);
|
||||
--md-list-item-trailing-space: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -506,7 +508,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
z-index: 1;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
@@ -520,12 +522,12 @@ class MoreInfoUpdate extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
mwc-linear-progress {
|
||||
margin-bottom: -8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: calc(var(--ha-space-2) * -1);
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
ha-markdown {
|
||||
direction: ltr;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-markdown.hidden {
|
||||
@@ -534,7 +536,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
.loader {
|
||||
height: 80px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ class MoreInfoValve extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
.main-control > * {
|
||||
margin: 0 8px;
|
||||
margin: 0 var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -214,7 +214,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: var(--ha-space-10);
|
||||
}
|
||||
|
||||
.current div {
|
||||
@@ -237,7 +237,7 @@ class MoreInfoWaterHeater extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.4px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.current .value {
|
||||
|
||||
@@ -440,13 +440,13 @@ class MoreInfoWeather extends LitElement {
|
||||
css`
|
||||
ha-svg-icon {
|
||||
color: var(--state-icon-color);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 16px 0 8px 0;
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-2) 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@@ -470,14 +470,14 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.attribution {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.time-ago,
|
||||
@@ -500,15 +500,15 @@ class MoreInfoWeather extends LitElement {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 64px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-right: var(--ha-space-4);
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
.temp-attribute .temp {
|
||||
position: relative;
|
||||
margin-right: 24px;
|
||||
margin-right: var(--ha-space-6);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -557,8 +557,8 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
.name-state {
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
padding-inline-end: 12px;
|
||||
padding-right: var(--ha-space-3);
|
||||
padding-inline-end: var(--ha-space-3);
|
||||
padding-inline-start: initial;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -572,7 +572,7 @@ class MoreInfoWeather extends LitElement {
|
||||
.forecast {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
padding-bottom: 0px;
|
||||
overflow-x: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
@@ -634,8 +634,8 @@ class MoreInfoWeather extends LitElement {
|
||||
}
|
||||
|
||||
.forecast-image-icon {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
padding-top: var(--ha-space-1);
|
||||
padding-bottom: var(--ha-space-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -802,8 +802,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: var(--ha-space-0) var(--ha-space-0)
|
||||
calc(var(--ha-space-2) * -1) var(--ha-space-0);
|
||||
margin: 0 0 calc(var(--ha-space-2) * -1) 0;
|
||||
}
|
||||
|
||||
.title p {
|
||||
|
||||
@@ -58,7 +58,7 @@ export class MoreInfoHistoryAndLogbook extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
ha-more-info-history + ha-more-info-logbook {
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ export class MoreInfoHistory extends LitElement {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.header > a,
|
||||
a:visited {
|
||||
|
||||
@@ -130,8 +130,8 @@ export class MoreInfoInfo extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), 24px);
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
[data-domain="camera"] .content {
|
||||
@@ -153,7 +153,7 @@ export class MoreInfoInfo extends LitElement {
|
||||
ha-more-info-history,
|
||||
ha-more-info-logbook:not(:last-child) {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
|
||||
@@ -82,7 +82,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.header > a,
|
||||
a:visited {
|
||||
|
||||
@@ -83,7 +83,8 @@ export class HaMoreInfoSettings extends LitElement {
|
||||
return [
|
||||
css`
|
||||
.content {
|
||||
padding: 8px 24px 24px 24px;
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
|
||||
var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -119,7 +119,7 @@ class MoreInfoContent extends LitElement {
|
||||
hui-section {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1120,7 +1120,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
.nothing-found {
|
||||
padding: var(--ha-space-4) var(--ha-space-0);
|
||||
padding: var(--ha-space-4) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ export class CloudStepSignin extends LitElement {
|
||||
),
|
||||
});
|
||||
if (totpCode !== null && totpCode !== "") {
|
||||
await doLogin(username, totpCode);
|
||||
await doLogin(username, totpCode.trim());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
.fade-top,
|
||||
.fade-bottom {
|
||||
position: absolute;
|
||||
left: var(--ha-space-0);
|
||||
right: var(--ha-space-0);
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--ha-space-4);
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
@@ -137,10 +137,10 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
opacity: 0;
|
||||
}
|
||||
.fade-top {
|
||||
top: var(--ha-space-0);
|
||||
top: 0;
|
||||
}
|
||||
.fade-bottom {
|
||||
bottom: var(--ha-space-0);
|
||||
bottom: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type {
|
||||
FloorRegistryEntry,
|
||||
@@ -49,6 +50,8 @@ class DialogFloorDetail extends LitElement {
|
||||
|
||||
@state() private _removedAreas = new Set<string>();
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: FloorRegistryDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
@@ -60,9 +63,14 @@ class DialogFloorDetail extends LitElement {
|
||||
this._level = this._params.entry?.level ?? null;
|
||||
this._addedAreas.clear();
|
||||
this._removedAreas.clear();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
this._addedAreas.clear();
|
||||
@@ -96,18 +104,15 @@ class DialogFloorDetail extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.floors.editor.update_floor")
|
||||
: this.hass.localize("ui.panel.config.floors.editor.create_floor")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.floors.editor.update_floor")
|
||||
: this.hass.localize("ui.panel.config.floors.editor.create_floor")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
@@ -128,6 +133,7 @@ class DialogFloorDetail extends LitElement {
|
||||
: nothing}
|
||||
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.floors.editor.name")}
|
||||
@@ -135,7 +141,6 @@ class DialogFloorDetail extends LitElement {
|
||||
"ui.panel.config.floors.editor.name_required"
|
||||
)}
|
||||
required
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
@@ -230,23 +235,25 @@ class DialogFloorDetail extends LitElement {
|
||||
></ha-aliases-editor>
|
||||
</div>
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.create")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.create")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -285,10 +292,6 @@ class DialogFloorDetail extends LitElement {
|
||||
this._addedAreas = new Set(this._addedAreas);
|
||||
}
|
||||
|
||||
private _isNameValid() {
|
||||
return this._name.trim() !== "";
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._name = ev.target.value;
|
||||
@@ -305,7 +308,16 @@ class DialogFloorDetail extends LitElement {
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
if (this._name.trim() === "") {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.floors.editor.name_required"
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
|
||||
this._submitting = true;
|
||||
|
||||
const create = !this._params!.entry;
|
||||
try {
|
||||
const values: FloorRegistryEntryMutableParams = {
|
||||
@@ -344,13 +356,13 @@ class DialogFloorDetail extends LitElement {
|
||||
css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-floor-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -2016,12 +2016,12 @@ class DialogAddAutomationElement
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
--ha-bottom-sheet-max-width: 888px;
|
||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||
--ha-bottom-sheet-padding: 0;
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
}
|
||||
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: var(--ha-space-0);
|
||||
--dialog-content-padding: 0;
|
||||
--ha-dialog-min-height: min(
|
||||
800px,
|
||||
calc(
|
||||
@@ -2045,7 +2045,7 @@ class DialogAddAutomationElement
|
||||
|
||||
search-input {
|
||||
display: block;
|
||||
margin: var(--ha-space-0) var(--ha-space-4);
|
||||
margin: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-button-toggle-group {
|
||||
@@ -2079,7 +2079,7 @@ class DialogAddAutomationElement
|
||||
.groups {
|
||||
overflow: auto;
|
||||
flex: 4;
|
||||
margin-inline-end: var(--ha-space-0);
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
ha-automation-add-from-target.hidden {
|
||||
|
||||
@@ -213,8 +213,8 @@ export class HaAutomationAddItems extends LitElement {
|
||||
background-color: var(--ha-color-surface-default);
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
padding: var(--ha-space-0);
|
||||
margin: var(--ha-space-0) var(--ha-space-4)
|
||||
padding: 0;
|
||||
margin: 0 var(--ha-space-4)
|
||||
max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
justify-content: center;
|
||||
@@ -233,7 +233,7 @@ export class HaAutomationAddItems extends LitElement {
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--ha-md-list-item-gap: var(--ha-space-3);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
.items ha-md-list ha-md-list-item {
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
|
||||
@@ -327,7 +327,7 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
style=${styleMap({
|
||||
width: "var(--ha-space-12)",
|
||||
position: "absolute",
|
||||
top: "var(--ha-space-0)",
|
||||
top: "0",
|
||||
left: rtl ? undefined : "var(--ha-space-1)",
|
||||
right: rtl ? "var(--ha-space-1)" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
|
||||
@@ -256,6 +256,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.required=${dataField.required}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
|
||||
@@ -292,6 +292,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.required=${dataField.required}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
|
||||
@@ -155,7 +155,7 @@ export class CloudLogin extends LitElement {
|
||||
),
|
||||
});
|
||||
if (totpCode !== null && totpCode !== "") {
|
||||
this._login(email, password, checkConnection, totpCode);
|
||||
this._login(email, password, checkConnection, totpCode.trim());
|
||||
return "continue";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
style=${styleMap({
|
||||
width: `${this._width}px`,
|
||||
height: this._narrow
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, var(--ha-space-0)) - var(--safe-area-inset-bottom, var(--ha-space-0)))"
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
|
||||
: "500px",
|
||||
})}
|
||||
@click=${this._integrationPicked}
|
||||
|
||||
@@ -199,7 +199,7 @@ export class DialogLabsPreviewFeatureEnable
|
||||
|
||||
static readonly styles = css`
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: var(--ha-space-0);
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -673,12 +673,12 @@ class HaPanelDevAction extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.button-row {
|
||||
padding: 8px 16px;
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color);
|
||||
@@ -698,8 +698,8 @@ class HaPanelDevAction extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
.switch-mode-container .error {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.attributes {
|
||||
@@ -732,7 +732,7 @@ class HaPanelDevAction extends LitElement {
|
||||
}
|
||||
|
||||
.attributes td {
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -748,7 +748,7 @@ class HaPanelDevAction extends LitElement {
|
||||
.response img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 24px;
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -240,13 +240,13 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 16px;
|
||||
padding: var(--ha-space-7) var(--ha-space-5) var(--ha-space-4);
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.description {
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
@@ -255,18 +255,18 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
text-align: right;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.result-toolbar {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.result {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.sentence {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@@ -280,7 +280,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
ha-code-editor,
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-debug-connection-row";
|
||||
import {
|
||||
getStatisticMetadata,
|
||||
validateStatistics,
|
||||
} from "../../../data/recorder";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { getExtendedEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
|
||||
@customElement("developer-tools-debug")
|
||||
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
@@ -12,6 +22,8 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
@@ -25,15 +37,83 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
></ha-debug-connection-row>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.debug.entity_diagnostic.title"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.debug.entity_diagnostic.description"
|
||||
)}
|
||||
@value-changed=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@click=${this._copyEntityDiagnostic}
|
||||
appearance="filled"
|
||||
.disabled=${!this._entityId}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.debug.entity_diagnostic.copy_to_clipboard"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _copyEntityDiagnostic() {
|
||||
const id = this._entityId!;
|
||||
let statistic;
|
||||
if (computeDomain(id) === "sensor") {
|
||||
const [metadata, issues] = await Promise.all([
|
||||
getStatisticMetadata(this.hass, [id]),
|
||||
validateStatistics(this.hass),
|
||||
]);
|
||||
const issue = issues[id];
|
||||
if (metadata || issue) {
|
||||
statistic = {
|
||||
metadata,
|
||||
issue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const entity = await getExtendedEntityRegistryEntry(this.hass, id);
|
||||
const device = entity?.device_id && this.hass.devices[entity.device_id];
|
||||
|
||||
const data = {
|
||||
state: this.hass.states[id],
|
||||
entity,
|
||||
device,
|
||||
statistic,
|
||||
};
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
await copyToClipboard(json);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _entityPicked(ev) {
|
||||
this._entityId = ev.detail.value;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.card-content {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
.content {
|
||||
padding: 28px 20px 16px;
|
||||
padding: var(--ha-space-7) var(--ha-space-5) var(--ha-space-4);
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -148,7 +148,7 @@ class HaPanelDevEvent extends LitElement {
|
||||
css`
|
||||
.content {
|
||||
gap: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class HaPanelDevEvent extends LitElement {
|
||||
}
|
||||
|
||||
ha-button {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
@@ -178,7 +178,7 @@ class HaPanelDevEvent extends LitElement {
|
||||
|
||||
event-subscribe-card {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,16 +160,16 @@ class EventSubscribeCard extends LitElement {
|
||||
static styles = css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.error-message {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.event {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
margin: 16px 0;
|
||||
padding-top: var(--ha-space-2);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
.event:last-child {
|
||||
border-bottom: 0;
|
||||
@@ -179,7 +179,7 @@ class EventSubscribeCard extends LitElement {
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
ha-card {
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -176,12 +176,12 @@ class PanelDeveloperTools extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 8px 12px;
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
|
||||
@@ -298,7 +298,7 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
}
|
||||
|
||||
.cell .padded {
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.entities .row .header:nth-child(1),
|
||||
@@ -328,11 +328,11 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
|
||||
.entities ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-right: var(--ha-space-2);
|
||||
margin-inline-end: var(--ha-space-2);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
|
||||
@@ -507,7 +507,7 @@ class HaPanelDevState extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
display: block;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host search-input {
|
||||
@@ -525,7 +525,7 @@ class HaPanelDevState extends LitElement {
|
||||
}
|
||||
|
||||
.heading ha-formfield {
|
||||
margin-right: 8px;
|
||||
margin-right: var(--ha-space-2);
|
||||
--mdc-typography-body2-font-size: var(--ha-font-size-m);
|
||||
--mdc-typography-body2-font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
@@ -534,9 +534,9 @@ class HaPanelDevState extends LitElement {
|
||||
display: block;
|
||||
font-family: var(--ha-font-family-code);
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 4px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-s);
|
||||
--mdc-icon-size: 14px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
@@ -557,15 +557,15 @@ class HaPanelDevState extends LitElement {
|
||||
}
|
||||
|
||||
.state-input {
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 0 8px 16px;
|
||||
margin: 0 var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-expansion-panel p {
|
||||
padding: 0 8px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
.inputs {
|
||||
@@ -574,12 +574,12 @@ class HaPanelDevState extends LitElement {
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
margin: 8px 0;
|
||||
margin: var(--ha-space-2) 0;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@@ -732,7 +732,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
gap: var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
@@ -751,7 +751,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
@@ -763,7 +763,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
box-sizing: border-box;
|
||||
font-size: var(--ha-font-size-m);
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
@@ -776,8 +776,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
.selection-controls p {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
|
||||
@@ -513,7 +513,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
.text-content,
|
||||
ha-selector-datetime,
|
||||
ha-selector-number {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
ha-list-item {
|
||||
margin: 0 -24px;
|
||||
@@ -522,7 +522,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
.table-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
.stat-list {
|
||||
min-height: 360px;
|
||||
|
||||
@@ -275,7 +275,7 @@ ${type === "object"
|
||||
|
||||
.content {
|
||||
gap: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.content.horizontal {
|
||||
@@ -289,7 +289,7 @@ ${type === "object"
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.edit-pane {
|
||||
@@ -307,14 +307,14 @@ ${type === "object"
|
||||
|
||||
.render-spinner {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-end: 8px;
|
||||
top: var(--ha-space-2);
|
||||
right: var(--ha-space-2);
|
||||
inset-inline-end: var(--ha-space-2);
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ ${type === "object"
|
||||
clear: both;
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 8px;
|
||||
padding: var(--ha-space-2);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
direction: ltr;
|
||||
|
||||
@@ -251,19 +251,19 @@ export class DeveloperYamlConfig extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 16px;
|
||||
padding: var(--ha-space-7) var(--ha-space-5) var(--ha-space-4);
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 24px;
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px;
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -31,7 +31,11 @@ import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../../../../common/util/xss";
|
||||
|
||||
export function getSuggestedMax(dayDifference: number, end: Date): number {
|
||||
export function getSuggestedMax(
|
||||
dayDifference: number,
|
||||
end: Date,
|
||||
detailedDailyData = false
|
||||
): number {
|
||||
let suggestedMax = new Date(end);
|
||||
|
||||
// Sometimes around DST we get a time of 0:59 instead of 23:59 as expected.
|
||||
@@ -40,7 +44,9 @@ export function getSuggestedMax(dayDifference: number, end: Date): number {
|
||||
suggestedMax = subHours(suggestedMax, 1);
|
||||
}
|
||||
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
if (!detailedDailyData) {
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
suggestedMax.setDate(1);
|
||||
}
|
||||
@@ -77,7 +83,8 @@ export function getCommonOptions(
|
||||
unit?: string,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date,
|
||||
formatTotal?: (total: number) => string
|
||||
formatTotal?: (total: number) => string,
|
||||
detailedDailyData = false
|
||||
): ECOption {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
|
||||
@@ -89,7 +96,7 @@ export function getCommonOptions(
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
max: getSuggestedMax(dayDifference, end),
|
||||
max: getSuggestedMax(dayDifference, end, detailedDailyData),
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import { endOfToday, isSameDay, isToday, startOfToday } from "date-fns";
|
||||
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -132,7 +132,9 @@ export class HuiPowerSourcesGraphCard
|
||||
config,
|
||||
"kW",
|
||||
compareStart,
|
||||
compareEnd
|
||||
compareEnd,
|
||||
undefined,
|
||||
true
|
||||
),
|
||||
legend: {
|
||||
show: this._config?.show_legend !== false,
|
||||
@@ -210,14 +212,17 @@ export class HuiPowerSourcesGraphCard
|
||||
const { positive, negative } = this._processData(
|
||||
statIds[key].stats.map((id: string) => {
|
||||
const stats = energyData.stats[id] ?? [];
|
||||
const currentStateWatts = getPowerFromState(this.hass.states[id]);
|
||||
if (currentStateWatts !== undefined) {
|
||||
// getPowerFromState returns power in W; convert to kW for this graph
|
||||
stats.push({
|
||||
start: now,
|
||||
end: now,
|
||||
mean: currentStateWatts / 1000,
|
||||
});
|
||||
if (isSameDay(now, this._start) && isSameDay(now, this._end)) {
|
||||
// Append current state if we are showing today
|
||||
const currentStateWatts = getPowerFromState(this.hass.states[id]);
|
||||
if (currentStateWatts !== undefined) {
|
||||
// getPowerFromState returns power in W; convert to kW for this graph
|
||||
stats.push({
|
||||
start: now,
|
||||
end: now,
|
||||
mean: currentStateWatts / 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
})
|
||||
|
||||
@@ -732,9 +732,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
|
||||
}
|
||||
.container.horizontal hui-card-features {
|
||||
width: calc(
|
||||
50% - var(--column-gap, var(--ha-space-0)) / 2 - var(--ha-space-3)
|
||||
);
|
||||
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
|
||||
flex: none;
|
||||
--feature-height: var(--ha-space-9);
|
||||
padding: 0 var(--ha-space-3);
|
||||
|
||||
@@ -141,30 +141,17 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
);
|
||||
const maxCommonControls = Math.max(8, favoriteEntities.length);
|
||||
|
||||
const commonControlsSectionBase = {
|
||||
const commonControlsSection = {
|
||||
strategy: {
|
||||
type: "common-controls",
|
||||
limit: maxCommonControls,
|
||||
include_entities: favoriteEntities,
|
||||
title: hass.localize("ui.panel.lovelace.strategy.home.favorites"),
|
||||
hide_empty: true,
|
||||
} satisfies CommonControlSectionStrategyConfig,
|
||||
column_span: maxColumns,
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const commonControlsSectionMobile = {
|
||||
...commonControlsSectionBase,
|
||||
strategy: {
|
||||
...commonControlsSectionBase.strategy,
|
||||
title: hass.localize("ui.panel.lovelace.strategy.home.commonly_used"),
|
||||
},
|
||||
visibility: [smallScreenCondition],
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const commonControlsSectionDesktop = {
|
||||
...commonControlsSectionBase,
|
||||
visibility: [largeScreenCondition],
|
||||
} as LovelaceStrategySectionConfig;
|
||||
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map(
|
||||
@@ -309,20 +296,8 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
|
||||
const sections = (
|
||||
[
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
// Heading to add some spacing on large screens
|
||||
{
|
||||
type: "heading",
|
||||
heading_style: "subtitle",
|
||||
visibility: [largeScreenCondition],
|
||||
},
|
||||
],
|
||||
},
|
||||
mobileSummarySection,
|
||||
commonControlsSectionMobile,
|
||||
commonControlsSectionDesktop,
|
||||
commonControlsSection,
|
||||
...floorsSections,
|
||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||
).filter(Boolean) as LovelaceSectionRawConfig[];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user