Compare commits

..

4 Commits

Author SHA1 Message Date
Aidan Timson
336a8e6241 Update src/panels/lovelace/card-features/hui-media-player-volume-buttons-card-feature.ts
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-24 13:45:09 +01:00
Aidan Timson
01ad51617a Add uom 2025-10-24 11:28:52 +01:00
Aidan Timson
be741de31d Sort import 2025-10-24 11:25:46 +01:00
Aidan Timson
772459f5a7 Add media player volume buttons card feature 2025-10-24 11:18:37 +01:00
44 changed files with 1497 additions and 1555 deletions

View File

@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: translations
path: translations.tar.gz

View File

@@ -28,8 +28,8 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/autocomplete": "6.19.0",
"@codemirror/commands": "6.9.0",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.5",
"@lezer/highlight": "1.2.2",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -148,16 +148,16 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.5",
"@babel/core": "7.28.4",
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3",
"@bundle-stats/plugin-webpack-filter": "4.21.5",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.2",
"@octokit/plugin-retry": "8.0.2",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.4",
"@rsdoctor/rspack-plugin": "1.3.3",
"@rspack/core": "1.5.8",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -173,12 +173,12 @@
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.2",
"@vitest/coverage-v8": "3.2.4",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -203,7 +203,7 @@
"husky": "9.1.7",
"jsdom": "27.0.1",
"jszip": "3.10.1",
"lint-staged": "16.2.6",
"lint-staged": "16.2.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -219,7 +219,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.46.2",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.2",
"vitest": "3.2.4",
"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"

View File

@@ -147,7 +147,6 @@ class HaEntitiesPicker extends LitElement {
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
add-button
></ha-entity-picker>
</div>
`;

View File

@@ -113,9 +113,6 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "add-button", type: Boolean })
public addButton = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
protected firstUpdated(changedProperties: PropertyValues): void {
@@ -284,7 +281,7 @@ export class HaEntityPicker extends LitElement {
.searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
@@ -292,9 +289,6 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined}
>
</ha-generic-picker>
`;

View File

@@ -46,7 +46,7 @@ export class HaAnalytics extends LitElement {
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${!!baseEnabled}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
@@ -70,7 +70,7 @@ export class HaAnalytics extends LitElement {
<ha-switch
.id="switch-${preference}"
@change=${this._handleRowClick}
.checked=${!!this.analytics?.preferences[preference]}
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
>
@@ -102,7 +102,7 @@ export class HaAnalytics extends LitElement {
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${!!this.analytics?.preferences.diagnostics}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"

View File

@@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement {
}
.row {
display: flex;
padding: var(--ha-space-0) var(--ha-space-2);
padding: 0 8px;
min-height: 48px;
align-items: center;
cursor: pointer;
@@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement {
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
margin-left: calc(var(--ha-space-2) * -1);
margin-left: -8px;
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: var(--ha-space-1);
padding: 4px;
display: flex;
justify-content: center;
align-items: center;
@@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: var(--ha-space-5);
--mdc-icon-size: 20px;
color: var(--white-color);
transform: rotate(-45deg);
}
@@ -170,7 +170,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 12px;
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -44,26 +44,26 @@ export class HaCard extends LitElement {
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
padding: 12px 16px 16px;
display: block;
margin-block-start: var(--ha-space-0);
margin-block-end: var(--ha-space-0);
margin-block-start: 0px;
margin-block-end: 0px;
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);
margin-top: calc(var(--ha-space-2) * -1);
padding-top: 0px;
margin-top: -8px;
}
:host ::slotted(.card-content) {
padding: var(--ha-space-4);
padding: 16px;
}
:host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8);
padding: var(--ha-space-2);
padding: 8px;
}
`;

View File

@@ -1,14 +1,10 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiPlaylistPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet";
import "./ha-button";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-input-helper-text";
@@ -19,7 +15,7 @@ import type {
PickerComboBoxSearchFn,
} from "./ha-picker-combo-box";
import "./ha-picker-field";
import type { PickerValueRenderer } from "./ha-picker-field";
import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
@customElement("ha-generic-picker")
@@ -57,7 +53,7 @@ export class HaGenericPicker extends LitElement {
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
@@ -68,130 +64,59 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
/** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-field") private _field?: HaPickerField;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@state() private _opened = false;
@state() private _pickerWrapperOpen = false;
@state() private _popoverWidth = 0;
@state() private _openedNarrow = false;
private _narrow = false;
// helper to set new value after closing picker, to avoid flicker
private _newValue?: string;
private _unsubscribeTinyKeys?: () => void;
protected render() {
return html`
${this.label
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
: nothing}
<div class="container">
<div id="picker">
<slot name="field">
${this.addButtonLabel && !this.value
? html`<ha-button
size="small"
appearance="filled"
@click=${this.open}
.disabled=${this.disabled}
>
<ha-svg-icon
.path=${mdiPlaylistPlus}
slot="start"
></ha-svg-icon>
${this.addButtonLabel}
</ha-button>`
: html`<ha-picker-field
type="button"
class=${this._opened ? "opened" : ""}
compact
aria-label=${ifDefined(this.label)}
@click=${this.open}
@clear=${this._clear}
.placeholder=${this.placeholder}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
.valueRenderer=${this.valueRenderer}
>
</ha-picker-field>`}
</slot>
</div>
${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
${!this._opened
? html`
<wa-popover
.open=${this._pickerWrapperOpen}
style="--body-width: ${this._popoverWidth}px;"
without-arrow
distance="-4"
placement="bottom-start"
for="picker"
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._dialogOpened}
@wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
<ha-picker-field
id="picker"
type="button"
compact
aria-label=${ifDefined(this.label)}
@click=${this.open}
@clear=${this._clear}
.placeholder=${this.placeholder}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
.valueRenderer=${this.valueRenderer}
>
${this._renderComboBox()}
</wa-popover>
</ha-picker-field>
`
: this._pickerWrapperOpen || this._opened
? html`<ha-bottom-sheet
flexcontent
.open=${this._pickerWrapperOpen}
@wa-after-show=${this._dialogOpened}
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>`
: nothing}
: html`
<ha-picker-combo-box
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ??
this.hass.localize("ui.common.search")}
.value=${this.value}
hide-clear-icon
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer}
.notFoundLabel=${this.notFoundLabel}
.getItems=${this.getItems}
.getAdditionalItems=${this.getAdditionalItems}
.searchFn=${this.searchFn}
></ha-picker-combo-box>
`}
</div>
${this._renderHelper()}
`;
}
private _renderComboBox(dialogMode = false) {
if (!this._opened) {
return nothing;
}
return html`
<ha-picker-combo-box
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
.value=${this.value}
@value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer}
.notFoundLabel=${this.notFoundLabel}
.getItems=${this.getItems}
.getAdditionalItems=${this.getAdditionalItems}
.searchFn=${this.searchFn}
.mode=${dialogMode ? "dialog" : "popover"}
></ha-picker-combo-box>
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
@@ -200,33 +125,13 @@ export class HaGenericPicker extends LitElement {
: nothing;
}
private _dialogOpened = () => {
this._opened = true;
requestAnimationFrame(() => {
this._comboBox?.focus();
});
};
private _hidePicker(ev) {
ev.stopPropagation();
if (this._newValue) {
fireEvent(this, "value-changed", { value: this._newValue });
this._newValue = undefined;
}
this._opened = false;
this._pickerWrapperOpen = false;
this._unsubscribeTinyKeys?.();
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value) {
return;
}
this._pickerWrapperOpen = false;
this._newValue = value;
fireEvent(this, "value-changed", { value });
}
private _clear(e) {
@@ -239,44 +144,24 @@ export class HaGenericPicker extends LitElement {
fireEvent(this, "value-changed", { value });
}
public async open(ev?: Event) {
ev?.stopPropagation();
public async open() {
if (this.disabled) {
return;
}
this._openedNarrow = this._narrow;
this._popoverWidth = this._containerElement?.offsetWidth || 250;
this._pickerWrapperOpen = true;
this._unsubscribeTinyKeys = tinykeys(this, {
Escape: this._handleEscClose,
});
this._opened = true;
await this.updateComplete;
this._comboBox?.focus();
this._comboBox?.open();
}
connectedCallback() {
super.connectedCallback();
this._handleResize();
window.addEventListener("resize", this._handleResize);
}
public disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this._handleResize);
this._unsubscribeTinyKeys?.();
}
private _handleResize = () => {
this._narrow =
window.matchMedia("(max-width: 870px)").matches ||
window.matchMedia("(max-height: 500px)").matches;
if (!this._openedNarrow && this._pickerWrapperOpen) {
this._popoverWidth = this._containerElement?.offsetWidth || 250;
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._field?.focus();
}
};
private _handleEscClose = (ev: KeyboardEvent) => {
ev.stopPropagation();
};
}
static get styles(): CSSResultGroup {
return [
@@ -296,44 +181,6 @@ export class HaGenericPicker extends LitElement {
display: block;
margin: var(--ha-space-2) 0 0;
}
wa-popover {
--wa-space-l: var(--ha-space-0);
}
wa-popover::part(body) {
width: max(var(--body-width), 250px);
max-width: max(var(--body-width), 250px);
max-height: 500px;
height: 70vh;
overflow: hidden;
}
@media (max-height: 1000px) {
wa-popover::part(body) {
max-height: 400px;
}
}
@media (max-height: 1000px) {
wa-popover::part(body) {
max-height: 400px;
}
}
ha-bottom-sheet {
--ha-bottom-sheet-height: 90vh;
--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-surface-background: var(--card-background-color);
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
}
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
`,
];
}

View File

@@ -2,13 +2,7 @@ import { mdiLabel, mdiPlus } from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import {
customElement,
property,
query,
queryAssignedElements,
state,
} from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { LabelRegistryEntry } from "../data/label_registry";
@@ -90,9 +84,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
@state() private _labels?: LabelRegistryEntry[];
@queryAssignedElements({ flatten: true })
private _slotNodes?: NodeListOf<HTMLElement>;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
public async open() {
@@ -220,14 +211,12 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
return html`
<ha-generic-picker
.disabled=${this.disabled}
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this.hass.localize(
"ui.components.label-picker.no_match"
)}
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
@@ -235,7 +224,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.valueRenderer=${valueRenderer}
@value-changed=${this._valueChanged}
>
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
</ha-generic-picker>
`;
}

View File

@@ -1,4 +1,3 @@
import { mdiPlaylistPlus } from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -124,6 +123,36 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${labels?.length
? html`<ha-chip-set>
${repeat(
labels,
(label) => label?.label_id,
(label) => {
const color = label?.color
? computeCssColor(label.color)
: undefined;
return html`
<ha-input-chip
.item=${label}
@remove=${this._removeItem}
@click=${this._openDetail}
.label=${label?.name}
selected
style=${color ? `--color: ${color}` : ""}
>
${label?.icon
? html`<ha-icon
slot="icon"
.icon=${label.icon}
></ha-icon>`
: nothing}
</ha-input-chip>
`;
}
)}
</ha-chip-set>`
: nothing}
<ha-label-picker
.hass=${this.hass}
.helper=${this.helper}
@@ -133,47 +162,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
.excludeLabels=${this.value}
@value-changed=${this._labelChanged}
>
<ha-chip-set>
${labels?.length
? repeat(
labels,
(label) => label?.label_id,
(label) => {
const color = label?.color
? computeCssColor(label.color)
: undefined;
return html`
<ha-input-chip
.item=${label}
@remove=${this._removeItem}
@click=${this._openDetail}
.disabled=${this.disabled}
.label=${label?.name}
selected
style=${color ? `--color: ${color}` : ""}
>
${label?.icon
? html`<ha-icon
slot="icon"
.icon=${label.icon}
></ha-icon>`
: nothing}
</ha-input-chip>
`;
}
)
: nothing}
<ha-button
id="picker"
size="small"
appearance="filled"
@click=${this._openPicker}
.disabled=${this.disabled}
>
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.components.label-picker.add")}
</ha-button>
</ha-chip-set>
</ha-label-picker>
`;
}
@@ -215,25 +203,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
}, 0);
}
private _openPicker(ev: Event) {
ev.stopPropagation();
this.labelPicker.open();
}
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);
border-top-left-radius: var(--ha-border-radius-sm);
padding: var(--ha-space-3);
}
.placeholder {
color: var(--mdc-text-field-label-ink-color);
display: flex;
align-items: center;
height: var(--ha-space-8);
}
ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color));

View File

@@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement {
}
ha-alert {
display: block;
margin: var(--ha-space-1) 0;
margin: 4px 0;
}
a {
color: var(--primary-color);
@@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement {
padding: 0;
}
pre {
padding: var(--ha-space-4);
padding: 16px;
overflow: auto;
line-height: var(--ha-line-height-condensed);
font-family: var(--ha-font-family-code);
@@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement {
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: var(--ha-space-4) 0;
margin: 16px 0;
}
` as CSSResultGroup;
}

View File

@@ -1,28 +1,19 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
export interface PickerComboBoxItem {
id: string;
@@ -42,13 +33,10 @@ export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
item
) => html`
<ha-combo-box-item
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
compact
>
<ha-combo-box-item type="button" compact>
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path
@@ -85,7 +73,7 @@ export class HaPickerComboBox extends LitElement {
@property() public value?: string;
@state() private _listScrolled = false;
@property() public helper?: string;
@property({ attribute: false, type: Array })
public getItems?: () => PickerComboBoxItem[];
@@ -94,7 +82,10 @@ export class HaPickerComboBox extends LitElement {
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
@@ -102,59 +93,23 @@ export class HaPickerComboBox extends LitElement {
@property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
@state() private _opened = false;
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@query("ha-textfield") private _searchFieldElement?: HaTextField;
@state() private _items: PickerComboBoxItemWithLabel[] = [];
private _allItems: PickerComboBoxItemWithLabel[] = [];
private _selectedItemIndex = -1;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
private _removeKeyboardShortcuts?: () => void;
protected firstUpdated() {
this._registerKeyboardShortcuts();
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public willUpdate() {
if (!this.hasUpdated) {
loadVirtualizer();
this._allItems = this._getItems();
this._items = this._allItems;
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
disconnectedCallback() {
super.disconnectedCallback();
this._removeKeyboardShortcuts?.();
}
private _initialItems = false;
protected render() {
return html`<ha-textfield
.label=${this.label ?? this.hass.localize("ui.common.search")}
@input=${this._filterChanged}
></ha-textfield>
<lit-virtualizer
@scroll=${this._onScrollList}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@focus=${this._focusList}
>
</lit-virtualizer> `;
}
private _items: PickerComboBoxItemWithLabel[] = [];
private _defaultNotFoundItem = memoizeOne(
(
@@ -204,56 +159,94 @@ export class HaPickerComboBox extends LitElement {
return sortedItems;
};
private _renderItem = (item: PickerComboBoxItem, index: number) => {
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
return html`<div
id=${`list-item-${index}`}
class="combo-box-row ${this._value === item.id ? "current-value" : ""}"
.value=${item.id}
.index=${index}
@click=${this._valueSelected}
>
${item.id === NO_MATCHING_ITEMS_FOUND_ID
? DEFAULT_ROW_RENDERER(item, index)
: renderer(item, index)}
</div>`;
};
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
@eventOptions({ passive: true })
private _onScrollList(ev) {
const top = ev.target.scrollTop ?? 0;
this._listScrolled = top > 0;
public willUpdate(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
this._items = this._getItems();
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult {
return html`
<ha-combo-box
item-id-path="id"
item-value-path="id"
item-label-path="a11y_label"
clear-initial-value
.hass=${this.hass}
.value=${this._value}
.label=${this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
.filteredItems=${this._items}
.renderer=${this.rowRenderer || DEFAULT_ROW_RENDERER}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
`;
}
private get _value() {
return this.value || "";
}
private _valueSelected = (ev: Event) => {
private _openedChanged(ev: ValueChangedEvent<boolean>) {
ev.stopPropagation();
const value = (ev.currentTarget as any).value as string;
const newValue = value?.trim();
if (ev.detail.value !== this._opened) {
this._opened = ev.detail.value;
fireEvent(this, "opened-changed", { value: this._opened });
}
}
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
// Clear the input field to prevent showing the old value next time
this.comboBox.setTextFieldValue("");
const newValue = ev.detail.value?.trim();
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
return;
}
fireEvent(this, "value-changed", { value: newValue });
};
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged = (ev: Event) => {
const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim();
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const index = this._fuseIndex(this._allItems);
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
const target = ev.target as HaComboBox;
const searchString = ev.detail.value.trim() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, { shouldSort: false }, index);
const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[];
let filteredItems = this._items as PickerComboBoxItem[];
if (results) {
const items = results.map((result) => result.item);
if (items.length === 0) {
@@ -267,266 +260,17 @@ export class HaPickerComboBox extends LitElement {
}
if (this.searchFn) {
filteredItems = this.searchFn(
searchString,
filteredItems,
this._allItems
);
filteredItems = this.searchFn(searchString, filteredItems, this._items);
}
this._items = filteredItems as PickerComboBoxItemWithLabel[];
this._selectedItemIndex = -1;
if (this._virtualizerElement) {
this._virtualizerElement.scrollTo(0, 0);
}
};
private _registerKeyboardShortcuts() {
this._removeKeyboardShortcuts = tinykeys(this, {
ArrowUp: this._selectPreviousItem,
ArrowDown: this._selectNextItem,
Home: this._selectFirstItem,
End: this._selectLastItem,
Enter: this._pickSelectedItem,
});
target.filteredItems = filteredItems;
}
private _focusList() {
if (this._selectedItemIndex === -1) {
this._selectNextItem();
}
private _setValue(value: string | undefined) {
setTimeout(() => {
fireEvent(this, "value-changed", { value });
}, 0);
}
private _selectNextItem = (ev?: KeyboardEvent) => {
ev?.stopPropagation();
ev?.preventDefault();
if (!this._virtualizerElement) {
return;
}
this._searchFieldElement?.focus();
const items = this._virtualizerElement.items as PickerComboBoxItem[];
const maxItems = items.length - 1;
if (maxItems === -1) {
this._resetSelectedItem();
return;
}
const nextIndex =
maxItems === this._selectedItemIndex
? this._selectedItemIndex
: this._selectedItemIndex + 1;
if (!items[nextIndex]) {
return;
}
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
// Skip titles, padding and empty search
if (nextIndex === maxItems) {
return;
}
this._selectedItemIndex = nextIndex + 1;
} else {
this._selectedItemIndex = nextIndex;
}
this._scrollToSelectedItem();
};
private _selectPreviousItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (!this._virtualizerElement) {
return;
}
if (this._selectedItemIndex > 0) {
const nextIndex = this._selectedItemIndex - 1;
const items = this._virtualizerElement.items as PickerComboBoxItem[];
if (!items[nextIndex]) {
return;
}
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
// Skip titles, padding and empty search
if (nextIndex === 0) {
return;
}
this._selectedItemIndex = nextIndex - 1;
} else {
this._selectedItemIndex = nextIndex;
}
this._scrollToSelectedItem();
}
};
private _selectFirstItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
return;
}
const nextIndex = 0;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex + 1;
} else {
this._selectedItemIndex = nextIndex;
}
this._scrollToSelectedItem();
};
private _selectLastItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
return;
}
const nextIndex = this._virtualizerElement.items.length - 1;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex - 1;
} else {
this._selectedItemIndex = nextIndex;
}
this._scrollToSelectedItem();
};
private _scrollToSelectedItem = () => {
this._virtualizerElement
?.querySelector(".selected")
?.classList.remove("selected");
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
requestAnimationFrame(() => {
this._virtualizerElement
?.querySelector(`#list-item-${this._selectedItemIndex}`)
?.classList.add("selected");
});
};
private _pickSelectedItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (this._selectedItemIndex === -1) {
return;
}
// if filter button is focused
ev.preventDefault();
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
fireEvent(this, "value-changed", { value: item.id });
}
};
private _resetSelectedItem() {
this._virtualizerElement
?.querySelector(".selected")
?.classList.remove("selected");
this._selectedItemIndex = -1;
}
static styles = [
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
flex: 1;
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
ha-combo-box-item {
width: 100%;
}
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
@media (prefers-color-scheme: dark) {
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
lit-virtualizer {
flex: 1;
}
lit-virtualizer:focus-visible {
outline: none;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
width: 100%;
}
.empty {
text-align: center;
}
.combo-box-row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
}
.combo-box-row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
@media (prefers-color-scheme: dark) {
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
`,
];
}
declare global {

View File

@@ -137,7 +137,7 @@ export class HaSelect extends SelectBase {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: var(--ha-space-4);
inset-inline-start: 12px;
inset-inline-end: initial;
direction: var(--direction);
}
@@ -147,7 +147,7 @@ export class HaSelect extends SelectBase {
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: var(--ha-space-4);
padding-inline-start: 12px;
padding-inline-end: 0px;
direction: var(--direction);
}
@@ -158,10 +158,7 @@ export class HaSelect extends SelectBase {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(
--select-selected-text-padding-end,
var(--ha-space-4)
);
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
ha-icon-button {
position: absolute;

View File

@@ -53,7 +53,7 @@ class HaServicePicker extends LitElement {
item,
{ index }
) => html`
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
<ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
<ha-service-icon
slot="start"
.hass=${this.hass}
@@ -76,42 +76,34 @@ class HaServicePicker extends LitElement {
</ha-combo-box-item>
`;
private _valueRenderer = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): PickerValueRenderer =>
(value) => {
const serviceId = value;
const [domain, service] = serviceId.split(".");
private _valueRenderer: PickerValueRenderer = (value) => {
const serviceId = value;
const [domain, service] = serviceId.split(".");
if (!services[domain]?.[service]) {
return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
if (!this.hass.services[domain]?.[service]) {
return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
const serviceName =
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
const serviceName =
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service].name ||
service;
return html`
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${serviceId}
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${this.showServiceId
? html`<span slot="supporting-text" class="code"
>${serviceId}</span
>`
: nothing}
`;
}
);
return html`
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${serviceId}
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${this.showServiceId
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
: nothing}
`;
};
protected render(): TemplateResult {
const placeholder =
@@ -131,10 +123,7 @@ class HaServicePicker extends LitElement {
.value=${this.value}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer(
this.hass.localize,
this.hass.services
)}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -264,7 +264,6 @@ export const getLabels = (
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: label.label_id,
primary: label.name,
secondary: label.description ?? "",
icon: label.icon || undefined,
icon_path: label.icon ? undefined : mdiLabel,
sorting_label: label.name,

View File

@@ -4,12 +4,12 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import type { DialogBoxParams } from "./show-dialog-box";
@@ -19,12 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _closePromise?: Promise<void>;
private _closeResolve?: () => void;
@@ -34,7 +34,6 @@ class DialogBox extends LitElement {
await this._closePromise;
}
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
@@ -61,16 +60,16 @@ class DialogBox extends LitElement {
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
?prevent-scrim-close=${confirmPrompt}
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<ha-dialog-header slot="header">
<span slot="title" id="dialog-box-title">
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -79,13 +78,13 @@ class DialogBox extends LitElement {
: nothing}
${dialogTitle}
</span>
</ha-dialog-header>
<div id="dialog-box-description">
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
autofocus
dialogInitialFocus
value=${ifDefined(this._params.defaultValue)}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel
@@ -100,11 +99,10 @@ class DialogBox extends LitElement {
`
: ""}
</div>
<ha-dialog-footer slot="footer">
<div slot="actions">
${confirmPrompt
? html`
<ha-button
slot="secondaryAction"
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
@@ -116,7 +114,6 @@ class DialogBox extends LitElement {
`
: nothing}
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
@@ -125,8 +122,8 @@ class DialogBox extends LitElement {
? this._params.confirmText
: this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -151,7 +148,8 @@ class DialogBox extends LitElement {
}
private _closeDialog() {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
this._closePromise = new Promise((resolve) => {
this._closeResolve = resolve;
});
@@ -164,7 +162,6 @@ class DialogBox extends LitElement {
}
this._closeState = undefined;
this._params = undefined;
this._open = false;
this._closeResolve?.();
this._closeResolve = undefined;
}

View File

@@ -678,8 +678,8 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: max(
var(--ha-space-10),
var(--safe-area-inset-top, var(--ha-space-0))
40px,
var(--safe-area-inset-top, 0px)
);
--dialog-content-padding: 0;
}
@@ -698,15 +698,14 @@ export class MoreInfoDialog extends LitElement {
}
ha-more-info-history-and-logbook {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
padding: 8px 24px 24px 24px;
display: block;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: var(--ha-space-0);
--dialog-surface-margin-top: 0px;
}
}
@@ -731,8 +730,7 @@ export class MoreInfoDialog extends 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 -10px 0;
}
.title p {
@@ -754,9 +752,9 @@ export class MoreInfoDialog extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 16px;
--mdc-icon-size: 16px;
padding: var(--ha-space-1);
margin: calc(var(--ha-space-1) * -1);
margin-top: calc(var(--ha-space-2) * -1);
padding: 4px;
margin: -4px;
margin-top: -10px;
background: none;
border: none;
outline: none;

View File

@@ -1011,8 +1011,8 @@ export class QuickBar extends LitElement {
--mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px;
--dialog-surface-position: fixed;
--dialog-surface-top: var(--ha-space-10);
--mdc-dialog-max-height: calc(100% - var(--ha-space-18));
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
}
}
@@ -1055,8 +1055,8 @@ export class QuickBar extends LitElement {
}
span.command-text {
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
direction: var(--direction);
}
@@ -1069,8 +1069,8 @@ export class QuickBar extends LitElement {
ha-md-list-item.two-line {
--md-list-item-one-line-container-height: 64px;
--md-list-item-two-line-container-height: 64px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
}
ha-md-list-item.three-line {
@@ -1078,8 +1078,8 @@ export class QuickBar extends LitElement {
--md-list-item-one-line-container-height: 72px;
--md-list-item-two-line-container-height: 72px;
--md-list-item-three-line-container-height: 72px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
}
ha-md-list-item .code {
@@ -1104,11 +1104,11 @@ export class QuickBar extends LitElement {
}
ha-tip {
padding: var(--ha-space-5);
padding: 20px;
}
.nothing-found {
padding: var(--ha-space-4) var(--ha-space-0);
padding: 16px 0px;
text-align: center;
}

View File

@@ -1,39 +1,40 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import type { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-floor-picker";
import "../../../components/ha-icon-picker";
import "../../../components/ha-labels-picker";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
import "../../../components/ha-icon-picker";
import "../../../components/ha-floor-picker";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-textfield";
import "../../../components/ha-labels-picker";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
} from "../../../data/area_registry";
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
import {
SENSOR_DEVICE_CLASS_HUMIDITY,
SENSOR_DEVICE_CLASS_TEMPERATURE,
} from "../../../data/sensor";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
import { createCloseHeading } from "../../../components/ha-dialog";
const cropOptions: CropOptions = {
round: false,
type: "image/jpeg",
quality: 0.75,
aspectRatio: 1.78,
};
const SENSOR_DOMAINS = ["sensor"];
@@ -138,7 +139,6 @@ class DialogAreaDetail extends LitElement {
></ha-floor-picker>
<ha-labels-picker
.label=${this.hass.localize("ui.components.label-picker.labels")}
.hass=${this.hass}
.value=${this._labels}
@value-changed=${this._labelsChanged}
@@ -265,15 +265,19 @@ class DialogAreaDetail extends LitElement {
${this.hass.localize("ui.common.delete")}
</ha-button>`
: nothing}
<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>
<div slot="primaryAction">
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</ha-button>
</div>
</ha-dialog>
`;
}

View File

@@ -98,7 +98,7 @@ class DialogCategoryDetail extends LitElement {
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}

View File

@@ -173,7 +173,7 @@ export class DialogEnergySolarSettings
<ha-checkbox
.entry=${entry}
@change=${this._forecastCheckChanged}
.checked=${!!this._source?.config_entry_solar_forecast?.includes(
.checked=${this._source?.config_entry_solar_forecast?.includes(
entry.entry_id
)}
>

View File

@@ -492,7 +492,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
)}</span
>
<ha-switch
.checked=${!!this.entry.options?.switch_as_x?.invert}
.checked=${this.entry.options?.switch_as_x?.invert}
@change=${this._switchAsInvertChanged}
></ha-switch>
</ha-settings-row>

View File

@@ -126,7 +126,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.entry=${this.entry}
.helperConfigEntry=${this._helperConfigEntry}
.disabled=${!!this._submitting}
.disabled=${this._submitting}
@change=${this._entityRegistryChanged}
></entity-registry-settings-editor>
</div>

View File

@@ -153,8 +153,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
},
yAxis: {
type: "value",
min: 0,
max: 100,
splitLine: {
show: true,
},

View File

@@ -191,7 +191,7 @@ class ZWaveJSCapabilityDoorLock extends LitElement {
<ha-switch
@change=${this._booleanChanged}
key="twistAssist"
.checked=${!!this._configuration?.twistAssist}
.checked=${this._configuration?.twistAssist}
>
</ha-switch>
</ha-formfield>
@@ -209,7 +209,7 @@ class ZWaveJSCapabilityDoorLock extends LitElement {
<ha-switch
@change=${this._booleanChanged}
key="blockToBlock"
.checked=${!!this._configuration?.blockToBlock}
.checked=${this._configuration?.blockToBlock}
>
</ha-switch>
</ha-formfield>

View File

@@ -3,7 +3,6 @@ import {
mdiDevices,
mdiDotsVertical,
mdiHelpCircle,
mdiLabelOutline,
mdiPlus,
mdiRobot,
mdiShape,
@@ -24,10 +23,8 @@ import type {
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-relative-time";
import type {
LabelRegistryEntry,
LabelRegistryEntryMutableParams,
@@ -46,6 +43,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
import type { HaMdMenu } from "../../../components/ha-md-menu";
@customElement("ha-config-labels")
export class HaConfigLabels extends LitElement {
@@ -102,9 +100,7 @@ export class HaConfigLabels extends LitElement {
label: localize("ui.panel.config.labels.headers.icon"),
type: "icon",
template: (label) =>
label.icon
? html`<ha-icon .icon=${label.icon}></ha-icon>`
: html`<ha-svg-icon .path=${mdiLabelOutline}></ha-svg-icon>`,
label.icon ? html`<ha-icon .icon=${label.icon}></ha-icon>` : nothing,
},
color: {
title: "",
@@ -112,18 +108,18 @@ export class HaConfigLabels extends LitElement {
label: localize("ui.panel.config.labels.headers.color"),
type: "icon",
template: (label) =>
html`<div
style=${styleMap({
backgroundColor: label.color
? computeCssColor(label.color)
: undefined,
borderRadius: "var(--ha-border-radius-md)",
border: "1px solid var(--outline-color)",
boxSizing: "border-box",
width: "var(--ha-space-5)",
height: "var(--ha-space-5)",
})}
></div>`,
label.color
? html`<div
style=${styleMap({
backgroundColor: computeCssColor(label.color),
borderRadius: "var(--ha-border-radius-md)",
border: "1px solid var(--outline-color)",
boxSizing: "border-box",
width: "var(--ha-space-5)",
height: "var(--ha-space-5)",
})}
></div>`
: nothing,
},
name: {
title: localize("ui.panel.config.labels.headers.name"),

View File

@@ -0,0 +1,126 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-number-buttons";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "./types";
import { clamp } from "../../../common/number/clamp";
export const supportsMediaPlayerVolumeButtonsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
@customElement("hui-media-player-volume-buttons-card-feature")
class HuiMediaPlayerVolumeButtonsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined;
}
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
return {
type: "media-player-volume-buttons",
step: 5,
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor"
);
return document.createElement(
"hui-media-player-volume-buttons-card-feature-editor"
);
}
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
) {
return nothing;
}
const position =
this._stateObj.attributes.volume_level != null
? Math.round(this._stateObj.attributes.volume_level * 100)
: undefined;
return html`
<ha-control-number-buttons
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
.locale=${this.hass.locale}
min="0"
max="100"
.step=${this._config.step ?? 5}
.value=${position}
unit="%"
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.hass!.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: clamp(ev.detail.value, 0, 100) / 100,
});
}
static get styles() {
return cardFeatureStyles;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature;
}
}

View File

@@ -50,6 +50,11 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
}
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
type: "media-player-volume-buttons";
step?: number;
}
export interface FanDirectionCardFeatureConfig {
type: "fan-direction";
}
@@ -252,6 +257,7 @@ export type LovelaceCardFeatureConfig =
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| MediaPlayerPlaybackCardFeatureConfig
| MediaPlayerVolumeButtonsCardFeatureConfig
| MediaPlayerVolumeSliderCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig

View File

@@ -34,7 +34,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
import { listenMediaQuery } from "../../../../common/dom/media_query";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -57,8 +56,6 @@ export class HuiEnergyDevicesGraphCard
})
private _chartType: "bar" | "pie" = "bar";
@state() private _isMobile = false;
private _compoundStats: string[] = [];
protected hassSubscribeRequiredHostProps = ["_config"];
@@ -71,12 +68,6 @@ export class HuiEnergyDevicesGraphCard
this._data = data;
this._getStatistics(data);
}),
listenMediaQuery(
"all and (max-width: 450px), all and (max-height: 500px)",
(matches) => {
this._isMobile = matches;
}
),
];
}
@@ -163,6 +154,9 @@ export class HuiEnergyDevicesGraphCard
yAxis: { show: false },
};
if (chartType === "bar") {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
options.xAxis = {
show: true,
type: "value",
@@ -181,7 +175,7 @@ export class HuiEnergyDevicesGraphCard
fontSize: 12,
margin: 5,
width: Math.min(
this._isMobile ? 100 : 200,
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
@@ -241,14 +235,8 @@ export class HuiEnergyDevicesGraphCard
this._chartType === "pie"
? {
formatter: ({ name }) => this._getDeviceName(name),
overflow: "break",
alignTo: this._isMobile ? "edge" : "none",
edgeDistance: 1,
}
: undefined,
labelLine: {
length2: 10,
},
} as BarSeriesOption | PieSeriesOption,
];

View File

@@ -660,7 +660,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: var(--ha-space-12);
--mdc-icon-size: 48px;
color: var(--tile-color);
}
.picture .icon-container::before {
@@ -729,15 +729,13 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
}
hui-card-features {
--feature-color: var(--tile-color);
padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
padding: 0 12px 12px 12px;
}
.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 - 12px);
flex: none;
--feature-height: var(--ha-space-9);
padding: 0 var(--ha-space-3);
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}
.alert-badge {
@@ -750,18 +748,18 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
display: flex;
flex-direction: row;
gap: var(--ha-space-2);
padding: var(--ha-space-2);
padding: 8px;
pointer-events: none;
z-index: 1;
}
.alert {
background-color: var(--orange-color);
border-radius: var(--ha-border-radius-lg);
width: var(--ha-space-6);
height: var(--ha-space-6);
width: 24px;
height: 24px;
padding: 2px;
box-sizing: border-box;
--mdc-icon-size: var(--ha-space-4);
--mdc-icon-size: 16px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -447,13 +447,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
hui-card-features {
--feature-color: var(--tile-color);
padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
padding: 0 12px 12px 12px;
}
.container.horizontal hui-card-features {
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
flex: none;
--feature-height: var(--ha-space-9);
padding: 0 var(--ha-space-3);
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}

View File

@@ -563,7 +563,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.name,
.attribute {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-condensed);
line-height: 1;
}
.name-state {
@@ -729,7 +729,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.short .state,
.short .temp-attribute .temp {
font-size: 24px;
line-height: var(--ha-line-height-condensed);
line-height: 1.25;
}
.short .content + .forecast {

View File

@@ -174,10 +174,10 @@ export class HuiEntityEditor extends LitElement {
</div>
</ha-sortable>`}
<ha-entity-picker
class="add-entity"
.hass=${this.hass}
.entityFilter=${this.entityFilter}
@value-changed=${this._addEntity}
add-button
></ha-entity-picker>
`;
}
@@ -226,6 +226,13 @@ export class HuiEntityEditor extends LitElement {
ha-entity-picker {
margin-top: 8px;
}
.add-entity {
display: block;
margin-left: 31px;
margin-inline-start: 31px;
margin-inline-end: initial;
direction: var(--direction);
}
.entity {
display: flex;
align-items: center;

View File

@@ -23,6 +23,7 @@ import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-media-player-playback-card-feature";
import "../card-features/hui-media-player-volume-buttons-card-feature";
import "../card-features/hui-media-player-volume-slider-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
@@ -72,6 +73,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",

View File

@@ -48,6 +48,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
@@ -102,6 +103,7 @@ const UI_FEATURE_TYPES = [
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
@@ -131,6 +133,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
"media-player-volume-buttons",
"numeric-input",
"select-options",
"trend-graph",
@@ -171,6 +174,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,
@@ -495,7 +499,7 @@ export class HuiCardFeaturesEditor extends LitElement {
flex-direction: column;
}
ha-button-menu {
margin-top: var(--ha-space-2);
margin-top: 8px;
}
.feature {
display: flex;
@@ -504,8 +508,8 @@ export class HuiCardFeaturesEditor extends LitElement {
.feature .handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-right: var(--ha-space-2);
padding-inline-end: var(--ha-space-2);
padding-right: 8px;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
@@ -514,7 +518,7 @@ export class HuiCardFeaturesEditor extends LitElement {
}
.feature-content {
height: var(--ha-space-15);
height: 60px;
font-size: var(--ha-font-size-l);
display: flex;
align-items: center;
@@ -529,7 +533,7 @@ export class HuiCardFeaturesEditor extends LitElement {
.remove-icon,
.edit-icon {
--mdc-icon-button-size: var(--ha-space-9);
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}

View File

@@ -1,12 +1,20 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import {
mdiDelete,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-sortable";
@@ -28,6 +36,14 @@ export class HuiHeadingBadgesEditor extends LitElement {
@property({ attribute: false })
public badges?: LovelaceHeadingBadgeConfig[];
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@query("ha-entity-picker") private _entityPicker?: HaEntityPicker;
@state() private _addMode = false;
private _opened = false;
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
private _getKey(badge: LovelaceHeadingBadgeConfig) {
@@ -109,6 +125,32 @@ export class HuiHeadingBadgesEditor extends LitElement {
`
: nothing}
<div class="add-container">
<ha-button
data-add-entity
appearance="filled"
@click=${this._addEntity}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass!.localize(`ui.panel.lovelace.editor.entities.add`)}
</ha-button>
${this._renderPicker()}
</div>
`;
}
private _renderPicker() {
if (!this._addMode) {
return nothing;
}
return html`
<mwc-menu-surface
open
.anchor=${this._addContainer}
@closed=${this._onClosed}
@opened=${this._onOpened}
@opened-changed=${this._openedChanged}
@input=${stopPropagation}
>
<ha-entity-picker
.hass=${this.hass}
id="input"
@@ -119,15 +161,39 @@ export class HuiHeadingBadgesEditor extends LitElement {
"ui.components.entity.entity-picker.choose_entity"
)}
@value-changed=${this._entityPicked}
.value=${undefined}
@click=${preventDefault}
allow-custom-entity
add-button
></ha-entity-picker>
</div>
</mwc-menu-surface>
`;
}
private _onClosed(ev) {
ev.stopPropagation();
ev.target.open = true;
}
private async _onOpened() {
if (!this._addMode) {
return;
}
await this._entityPicker?.focus();
await this._entityPicker?.open();
this._opened = true;
}
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
if (this._opened && !ev.detail.value) {
this._opened = false;
this._addMode = false;
}
}
private async _addEntity(ev): Promise<void> {
ev.stopPropagation();
this._addMode = true;
}
private _entityPicked(ev) {
ev.stopPropagation();
if (!ev.detail.value) {

View File

@@ -0,0 +1,86 @@
import { 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 "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-volume-buttons-card-feature-editor")
export class HuiMediaPlayerVolumeButtonsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
() =>
[
{
name: "step",
selector: {
number: {
mode: "slider",
step: 1,
min: 1,
max: 100,
unit_of_measurement: "%",
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data: MediaPlayerVolumeButtonsCardFeatureConfig = {
type: "media-player-volume-buttons",
step: this._config.step ?? 5,
};
const schema = this._schema();
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor;
}
}

View File

@@ -115,7 +115,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
class="add-entity"
.hass=${this.hass}
@value-changed=${this._addEntity}
add-button
></ha-entity-picker>
`;
}

View File

@@ -38,7 +38,7 @@ class AdvancedModeRow extends LitElement {
</a>
</span>
<ha-switch
.checked=${!!this.coreUserData && !!this.coreUserData.showAdvanced}
.checked=${this.coreUserData && this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined}
@change=${this._advancedToggled}
></ha-switch>

View File

@@ -32,8 +32,7 @@ class EntityIdPickerRow extends LitElement {
${this.hass.localize("ui.panel.profile.entity_id_picker.description")}
</span>
<ha-switch
.checked=${!!this.coreUserData &&
!!this.coreUserData.showEntityIdPicker}
.checked=${this.coreUserData && this.coreUserData.showEntityIdPicker}
.disabled=${this.coreUserData === undefined}
@change=${this._toggled}
></ha-switch>

View File

@@ -648,7 +648,6 @@
"entity-picker": {
"choose_entity": "Choose entity",
"entity": "Entity",
"add": "Add entity",
"edit": "Edit",
"clear": "Clear",
"no_entities": "You don't have any entities",
@@ -810,7 +809,6 @@
"labels": "Labels",
"add_new_sugestion": "Add new label ''{name}''",
"add_new": "Add new label…",
"add": "Add label",
"no_labels": "You don't have any labels",
"no_match": "No matching labels found",
"failed_create_label": "Failed to create label."
@@ -8153,6 +8151,10 @@
"media-player-playback": {
"label": "Media player playback controls"
},
"media-player-volume-buttons": {
"label": "Media player volume buttons",
"step": "Step size"
},
"media-player-volume-slider": {
"label": "Media player volume slider"
},

1533
yarn.lock

File diff suppressed because it is too large Load Diff