import { mdiAppleKeyboardCommand, mdiClose, mdiContentPaste, mdiPlus, } from "@mdi/js"; import Fuse from "fuse.js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, eventOptions, property, query, state, } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import { tinykeys } from "tinykeys"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stringCompare } from "../../../common/string/compare"; import type { LocalizeFunc, LocalizeKeys, } from "../../../common/translations/localize"; import { debounce } from "../../../common/util/debounce"; import { deepEqual } from "../../../common/util/deep-equal"; import "../../../components/ha-bottom-sheet"; import "../../../components/ha-button-toggle-group"; import "../../../components/ha-dialog-header"; import "../../../components/ha-domain-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button-prev"; import "../../../components/ha-icon-next"; import "../../../components/ha-md-divider"; import "../../../components/ha-md-list"; import type { HaMdList } from "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import "../../../components/ha-service-icon"; import "../../../components/ha-wa-dialog"; import "../../../components/search-input"; import { ACTION_BUILDING_BLOCKS_GROUP, ACTION_COLLECTIONS, ACTION_ICONS, SERVICE_PREFIX, getService, isService, } from "../../../data/action"; import type { AutomationElementGroup, AutomationElementGroupCollection, } from "../../../data/automation"; import { CONDITION_BUILDING_BLOCKS_GROUP, CONDITION_COLLECTIONS, CONDITION_ICONS, } from "../../../data/condition"; import { getServiceIcons } from "../../../data/icons"; import type { IntegrationManifest } from "../../../data/integration"; import { domainToName, fetchIntegrationManifests, } from "../../../data/integration"; import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger"; import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { HaFuse } from "../../../resources/fuse"; import type { HomeAssistant } from "../../../types"; import { isMac } from "../../../util/is_mac"; import { showToast } from "../../../util/toast"; import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import { PASTE_VALUE } from "./show-add-automation-element-dialog"; const TYPES = { trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS }, condition: { collections: CONDITION_COLLECTIONS, icons: CONDITION_ICONS, }, action: { collections: ACTION_COLLECTIONS, icons: ACTION_ICONS, }, }; interface ListItem { key: string; name: string; description: string; iconPath?: string; icon?: TemplateResult; } type DomainManifestLookup = Record; const ENTITY_DOMAINS_OTHER = new Set([ "date", "datetime", "device_tracker", "text", "time", "tts", "update", "weather", "image_processing", ]); const ENTITY_DOMAINS_MAIN = new Set(["notify"]); const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"]; @customElement("add-automation-element-dialog") class DialogAddAutomationElement extends KeyboardShortcutMixin(LitElement) implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _params?: AddAutomationElementDialogParams; @state() private _selectedCollectionIndex?: number; @state() private _selectedGroup?: string; @state() private _tab: "groups" | "blocks" = "groups"; @state() private _filter = ""; @state() private _manifests?: DomainManifestLookup; @state() private _domains?: Set; @state() private _open = true; @state() private _itemsScrolled = false; @state() private _bottomSheetMode = false; @state() private _narrow = false; @query(".items ha-md-list ha-md-list-item") private _itemsListFirstElement?: HaMdList; @query(".items") private _itemsListElement?: HTMLDivElement; private _fullScreen = false; private _removeKeyboardShortcuts?: () => void; public showDialog(params): void { this._params = params; this.addKeyboardShortcuts(); if (this._params?.type === "action") { this.hass.loadBackendTranslation("services"); this._fetchManifests(); this._calculateUsedDomains(); getServiceIcons(this.hass); } this._fullScreen = matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" ).matches; window.addEventListener("resize", this._updateNarrow); this._updateNarrow(); // prevent view mode switch when resizing window this._bottomSheetMode = this._narrow; } public closeDialog() { this.removeKeyboardShortcuts(); if (this._params) { fireEvent(this, "dialog-closed", { dialog: this.localName }); } this._open = true; this._itemsScrolled = false; this._bottomSheetMode = false; this._params = undefined; this._selectedGroup = undefined; this._tab = "groups"; this._selectedCollectionIndex = undefined; this._filter = ""; this._manifests = undefined; this._domains = undefined; return true; } private _getGroups = ( type: AddAutomationElementDialogParams["type"], group?: string, collectionIndex?: number ): AutomationElementGroup => { if (group && collectionIndex !== undefined) { return ( TYPES[type].collections[collectionIndex].groups[group].members || { [group]: {}, } ); } return TYPES[type].collections.reduce( (acc, collection) => ({ ...acc, ...collection.groups }), {} as AutomationElementGroup ); }; private _convertToItem = ( key: string, options, type: AddAutomationElementDialogParams["type"], localize: LocalizeFunc ): ListItem => ({ key, name: localize( // @ts-ignore `ui.panel.config.automation.editor.${type}s.${ options.members ? "groups" : "type" }.${key}.label` ), description: localize( // @ts-ignore `ui.panel.config.automation.editor.${type}s.${ options.members ? "groups" : "type" }.${key}.description${options.members ? "" : ".picker"}` ), iconPath: options.icon || TYPES[type].icons[key], }); private _getFilteredItems = memoizeOne( ( type: AddAutomationElementDialogParams["type"], filter: string, localize: LocalizeFunc, services: HomeAssistant["services"], manifests?: DomainManifestLookup ): ListItem[] => { const items = this._items(type, localize, services, manifests); const index = this._fuseIndex(items); const fuse = new HaFuse( items, { ignoreLocation: true, includeScore: true, minMatchCharLength: Math.min(2, this._filter.length), }, index ); const results = fuse.multiTermsSearch(filter); if (results) { return results.map((result) => result.item); } return items; } ); private _getFilteredBuildingBlocks = memoizeOne( ( type: AddAutomationElementDialogParams["type"], filter: string, localize: LocalizeFunc ): ListItem[] => { const groups = type === "action" ? ACTION_BUILDING_BLOCKS_GROUP : type === "condition" ? CONDITION_BUILDING_BLOCKS_GROUP : {}; const items = Object.keys(groups).map((key) => this._convertToItem(key, {}, type, localize) ); const index = this._fuseIndexBlock(items); const fuse = new HaFuse( items, { ignoreLocation: true, includeScore: true, minMatchCharLength: Math.min(2, this._filter.length), }, index ); const results = fuse.multiTermsSearch(filter); if (results) { return results.map((result) => result.item); } return items; } ); private _items = memoizeOne( ( type: AddAutomationElementDialogParams["type"], localize: LocalizeFunc, services: HomeAssistant["services"], manifests?: DomainManifestLookup ): ListItem[] => { const groups = this._getGroups(type); const flattenGroups = (grp: AutomationElementGroup) => Object.entries(grp).map(([key, options]) => options.members ? flattenGroups(options.members) : this._convertToItem(key, options, type, localize) ); const items = flattenGroups(groups).flat(); if (type === "action") { items.push(...this._services(localize, services, manifests)); } return items; } ); private _fuseIndex = memoizeOne((items: ListItem[]) => Fuse.createIndex(["key", "name", "description"], items) ); private _fuseIndexBlock = memoizeOne((items: ListItem[]) => Fuse.createIndex(["key", "name", "description"], items) ); private _getCollections = memoizeOne( ( type: AddAutomationElementDialogParams["type"], collections: AutomationElementGroupCollection[], domains: Set | undefined, localize: LocalizeFunc, services: HomeAssistant["services"], manifests?: DomainManifestLookup ): { titleKey?: LocalizeKeys; groups: ListItem[]; }[] => { const generatedCollections: any = []; collections.forEach((collection) => { let collectionGroups = Object.entries(collection.groups); const groups: ListItem[] = []; if ( type === "action" && Object.keys(collection.groups).some((item) => ACTION_SERVICE_KEYWORDS.includes(item) ) ) { groups.push( ...this._serviceGroups( localize, services, manifests, domains, collection.groups.serviceGroups ? undefined : collection.groups.helpers ? "helper" : "other" ) ); collectionGroups = collectionGroups.filter( ([key]) => !ACTION_SERVICE_KEYWORDS.includes(key) ); } groups.push( ...collectionGroups.map(([key, options]) => this._convertToItem(key, options, type, localize) ) ); generatedCollections.push({ titleKey: collection.titleKey, groups: groups.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language) ), }); }); return generatedCollections; } ); private _getBlockItems = memoizeOne( ( type: AddAutomationElementDialogParams["type"], localize: LocalizeFunc ): ListItem[] => { const groups = type === "action" ? ACTION_BUILDING_BLOCKS_GROUP : CONDITION_BUILDING_BLOCKS_GROUP; const result = Object.entries(groups).map(([key, options]) => this._convertToItem(key, options, type, localize) ); return result.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language) ); } ); private _getGroupItems = memoizeOne( ( type: AddAutomationElementDialogParams["type"], group: string, collectionIndex: number, domains: Set | undefined, localize: LocalizeFunc, services: HomeAssistant["services"], manifests?: DomainManifestLookup ): ListItem[] => { if (type === "action" && isService(group)) { return this._services(localize, services, manifests, group); } const groups = this._getGroups(type, group, collectionIndex); const result = Object.entries(groups).map(([key, options]) => this._convertToItem(key, options, type, localize) ); if (type === "action") { if (!this._selectedGroup) { result.unshift( ...this._serviceGroups( localize, services, manifests, domains, undefined ) ); } else if (this._selectedGroup === "helpers") { result.unshift( ...this._serviceGroups( localize, services, manifests, domains, "helper" ) ); } else if (this._selectedGroup === "other") { result.unshift( ...this._serviceGroups( localize, services, manifests, domains, "other" ) ); } } return result.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language) ); } ); private _serviceGroups = ( localize: LocalizeFunc, services: HomeAssistant["services"], manifests: DomainManifestLookup | undefined, domains: Set | undefined, type: "helper" | "other" | undefined ): ListItem[] => { if (!services || !manifests) { return []; } const result: ListItem[] = []; Object.keys(services).forEach((domain) => { const manifest = manifests[domain]; const domainUsed = !domains ? true : domains.has(domain); if ( (type === undefined && (ENTITY_DOMAINS_MAIN.has(domain) || (manifest?.integration_type === "entity" && domainUsed && !ENTITY_DOMAINS_OTHER.has(domain)))) || (type === "helper" && manifest?.integration_type === "helper") || (type === "other" && !ENTITY_DOMAINS_MAIN.has(domain) && (ENTITY_DOMAINS_OTHER.has(domain) || (!domainUsed && manifest?.integration_type === "entity") || !["helper", "entity"].includes(manifest?.integration_type || ""))) ) { result.push({ icon: html` `, key: `${SERVICE_PREFIX}${domain}`, name: domainToName(localize, domain, manifest), description: "", }); } }); return result.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language) ); }; private _services = memoizeOne( ( localize: LocalizeFunc, services: HomeAssistant["services"], manifests: DomainManifestLookup | undefined, group?: string ): ListItem[] => { if (!services) { return []; } const result: ListItem[] = []; let domain: string | undefined; if (isService(group)) { domain = getService(group!); } const addDomain = (dmn: string) => { const services_keys = Object.keys(services[dmn]); for (const service of services_keys) { result.push({ icon: html` `, key: `${SERVICE_PREFIX}${dmn}.${service}`, name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${ this.hass.localize(`component.${dmn}.services.${service}.name`) || services[dmn][service]?.name || service }`, description: this.hass.localize( `component.${dmn}.services.${service}.description` ) || services[dmn][service]?.description || "", }); } }; if (domain) { addDomain(domain); return result.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language) ); } if (group && !["helpers", "other"].includes(group)) { return []; } Object.keys(services) .sort() .forEach((dmn) => { const manifest = manifests?.[dmn]; if (group === "helpers" && manifest?.integration_type !== "helper") { return; } if ( group === "other" && (ENTITY_DOMAINS_OTHER.has(dmn) || ["helper", "entity"].includes(manifest?.integration_type || "")) ) { return; } addDomain(dmn); }); return result; } ); private async _fetchManifests() { const manifests = {}; const fetched = await fetchIntegrationManifests(this.hass); for (const manifest of fetched) { manifests[manifest.domain] = manifest; } this._manifests = manifests; } private _calculateUsedDomains() { const domains = new Set(Object.keys(this.hass.states).map(computeDomain)); if (!deepEqual(domains, this._domains)) { this._domains = domains; } } protected willUpdate(changedProperties: PropertyValues): void { if ( this._params?.type === "action" && changedProperties.has("hass") && changedProperties.get("hass")?.states !== this.hass.states ) { this._calculateUsedDomains(); } } private _renderContent() { const automationElementType = this._params!.type; const items = this._filter ? this._getFilteredItems( automationElementType, this._filter, this.hass.localize, this.hass.services, this._manifests ) : this._tab === "blocks" ? this._getBlockItems(automationElementType, this.hass.localize) : this._selectedGroup ? this._getGroupItems( automationElementType, this._selectedGroup, this._selectedCollectionIndex ?? 0, this._domains, this.hass.localize, this.hass.services, this._manifests ) : undefined; const filteredBlockItems = this._filter && automationElementType !== "trigger" ? this._getFilteredBuildingBlocks( automationElementType, this._filter, this.hass.localize ) : undefined; const collections = this._getCollections( automationElementType, TYPES[automationElementType].collections, this._domains, this.hass.localize, this.hass.services, this._manifests ); const groupName = isService(this._selectedGroup) ? domainToName( this.hass.localize, getService(this._selectedGroup!), this._manifests?.[getService(this._selectedGroup!)] ) : this.hass.localize( `ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys ) || this.hass.localize( `ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys ); const typeTitle = this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.header` ); const tabButtons = [ { label: this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.name` ), value: "groups", }, { label: this.hass.localize(`ui.panel.config.automation.editor.blocks`), value: "blocks", }, ]; const hideCollections = this._filter || this._tab === "blocks" || (this._narrow && this._selectedGroup); return html`
${this._narrow && this._selectedGroup ? groupName : typeTitle} ${this._narrow && this._selectedGroup ? html`${typeTitle}` : nothing} ${this._narrow && this._selectedGroup ? html`` : html``} ${!this._narrow || !this._selectedGroup ? html` ` : nothing} ${this._params?.type !== "trigger" && !this._filter && (!this._narrow || !this._selectedGroup) ? html`` : nothing}
${this._params!.clipboardItem && !this._filter ? html`
${this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.paste` )}
${this.hass.localize( // @ts-ignore `ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label` )}
${!this._narrow ? html` ${isMac ? html`` : this.hass.localize( "ui.panel.config.automation.editor.ctrl" )} + V ` : nothing}
` : nothing} ${collections.map( (collection, index) => html` ${collection.titleKey ? html`
${this.hass.localize(collection.titleKey)}
` : nothing} ${repeat( collection.groups, (item) => item.key, (item) => html`
${item.name}
${item.icon ? html`${item.icon}` : item.iconPath ? html`` : nothing}
` )} ` )}
${filteredBlockItems ? this._renderItemList( this.hass.localize(`ui.panel.config.automation.editor.blocks`), filteredBlockItems ) : nothing} ${this._tab === "groups" && !this._selectedGroup && !this._filter ? this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.select` ) : !items?.length && this._filter && (!filteredBlockItems || !filteredBlockItems.length) ? html`${this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.empty_search`, { term: html`‘${this._filter}’`, } )}` : this._renderItemList( this.hass.localize( `ui.panel.config.automation.editor.${automationElementType}s.name` ), items )}
`; } private _renderItemList(title, items?: ListItem[]) { if (!items || !items.length) { return nothing; } return html`
${title}
${repeat( items, (item) => item.key, (item) => html`
${item.name}
${item.description}
${item.icon ? html`${item.icon}` : item.iconPath ? html`` : nothing} ${item.group ? html`` : html``}
` )}
`; } protected render() { if (!this._params) { return nothing; } if (this._bottomSheetMode) { return html` ${this._renderContent()} `; } return html` ${this._renderContent()} `; } public disconnectedCallback(): void { super.disconnectedCallback(); window.removeEventListener("resize", this._updateNarrow); this._removeSearchKeybindings(); } private _updateNarrow = () => { this._narrow = window.matchMedia("(max-width: 870px)").matches || window.matchMedia("(max-height: 500px)").matches; }; private _close() { this._open = false; } private _back() { this._selectedGroup = undefined; } private _groupSelected(ev) { const group = ev.currentTarget; if (this._selectedGroup === group.value) { this._selectedGroup = undefined; this._selectedCollectionIndex = undefined; return; } this._selectedGroup = group.value; this._selectedCollectionIndex = ev.currentTarget.index; requestAnimationFrame(() => { this._itemsListElement?.scrollTo(0, 0); }); } private _selected(ev) { const item = ev.currentTarget; this._params!.add(item.value); this.closeDialog(); } private _debounceFilterChanged = debounce( (ev) => this._filterChanged(ev), 200 ); private _filterChanged = (ev) => { this._filter = ev.detail.value; }; private _addClipboard = () => { if (this._params?.clipboardItem) { this._params!.add(PASTE_VALUE); showToast(this, { message: this.hass.localize( "ui.panel.config.automation.editor.item_pasted", { item: this.hass.localize( // @ts-ignore `ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label` ), } ), }); this.closeDialog(); } }; protected supportedShortcuts(): SupportedShortcuts { return { v: () => this._addClipboard(), }; } private _switchTab(ev) { this._tab = ev.detail.value; } @eventOptions({ passive: true }) private _onItemsScroll(ev) { const top = ev.target.scrollTop ?? 0; this._itemsScrolled = top > 0; } private _onSearchFocus(ev) { this._removeKeyboardShortcuts = tinykeys(ev.target, { ArrowDown: this._focusSearchList, }); } private _removeSearchKeybindings() { this._removeKeyboardShortcuts?.(); } private _focusSearchList = (ev) => { if (!this._filter || !this._itemsListFirstElement) { return; } ev.preventDefault(); this._itemsListFirstElement.focus(); }; static get styles(): CSSResultGroup { return [ css` 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: 888px; --ha-bottom-sheet-padding: var(--ha-space-0); --ha-bottom-sheet-surface-background: var(--card-background-color); } ha-wa-dialog { --dialog-content-padding: var(--ha-space-0); --ha-dialog-width-md: 888px; --ha-dialog-min-height: min( 648px, calc( 100vh - max( var(--safe-area-inset-bottom), var(--ha-space-4) ) - max(var(--safe-area-inset-top), var(--ha-space-4)) ) ); --ha-dialog-min-height: min( 648px, calc( 100dvh - max( var(--safe-area-inset-bottom), var(--ha-space-4) ) - max(var(--safe-area-inset-top), var(--ha-space-4)) ) ); --ha-dialog-max-height: var(--ha-dialog-min-height); } search-input { display: block; margin: var(--ha-space-0) var(--ha-space-4); } ha-button-toggle-group { --ha-button-toggle-group-padding: var(--ha-space-3) var(--ha-space-4) 0; } .content { flex: 1; min-height: 0; height: 100%; display: flex; } ha-md-list { padding: 0; } .groups { overflow: auto; flex: 3; border-radius: var(--ha-border-radius-xl); border: 1px solid var(--ha-color-border-neutral-quiet); margin: var(--ha-space-3); margin-inline-end: var(--ha-space-0); --md-list-item-leading-space: var(--ha-space-3); --md-list-item-trailing-space: var(--md-list-item-leading-space); --md-list-item-bottom-space: var(--ha-space-1); --md-list-item-top-space: var(--md-list-item-bottom-space); --md-list-item-supporting-text-font: var(--ha-font-size-s); --md-list-item-one-line-container-height: var(--ha-space-10); } ha-bottom-sheet .groups { margin: var(--ha-space-3); } .groups .selected { background-color: var(--ha-color-fill-primary-normal-active); --md-list-item-label-text-color: var(--ha-color-on-primary-normal); --icon-primary-color: var(--ha-color-on-primary-normal); } .groups .selected ha-svg-icon { color: var(--ha-color-on-primary-normal); } .collection-title { background-color: var(--ha-color-fill-neutral-quiet-resting); padding: var(--ha-space-1) var(--ha-space-2); font-weight: var(--ha-font-weight-bold); color: var(--secondary-text-color); top: 0; position: sticky; min-height: var(--ha-space-6); display: flex; align-items: center; z-index: 1; } .items { display: flex; flex-direction: column; overflow: auto; flex: 7; } ha-wa-dialog .items { margin-top: var(--ha-space-3); } ha-bottom-sheet .groups { padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-4)); } .items.hidden, .groups.hidden { display: none; } .items.blank, .items.empty-search { border-radius: var(--ha-border-radius-xl); 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-3) var(--ha-space-4) max(var(--safe-area-inset-bottom), var(--ha-space-3)); } .items ha-md-list { --md-list-item-two-line-container-height: var(--ha-space-12); --md-list-item-leading-space: var(--ha-space-3); --md-list-item-trailing-space: var(--md-list-item-leading-space); --md-list-item-bottom-space: var(--ha-space-2); --md-list-item-top-space: var(--md-list-item-bottom-space); --md-list-item-supporting-text-font: var(--ha-font-size-s); gap: var(--ha-space-2); padding: var(--ha-space-0) var(--ha-space-4); } .items ha-md-list ha-md-list-item { border-radius: var(--ha-border-radius-lg); border: 1px solid var(--ha-color-border-neutral-quiet); } .items ha-md-list, .groups { padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3)); } .items.blank { justify-content: center; } .items.empty-search { padding-top: var(--ha-space-6); justify-content: start; } .items-title { position: sticky; display: flex; align-items: center; font-weight: var(--ha-font-weight-medium); padding-top: var(--ha-space-2); padding-bottom: var(--ha-space-2); padding-inline-start: var(--ha-space-8); padding-inline-end: var(--ha-space-3); top: 0; z-index: 1; background-color: var(--card-background-color); } ha-bottom-sheet .items-title { padding-top: var(--ha-space-3); } .items-title.scrolled:first-of-type { box-shadow: var(--bar-box-shadow); border-bottom: 1px solid var(--ha-color-border-neutral-quiet); } ha-icon-next { width: var(--ha-space-6); } ha-md-list-item.paste { border-bottom: 1px solid var(--ha-color-border-neutral-quiet); } ha-svg-icon.plus { color: var(--primary-color); } .shortcut-label { display: flex; gap: var(--ha-space-3); justify-content: space-between; } .shortcut-label .supporting-text { color: var(--secondary-text-color); font-size: var(--ha-font-size-s); } .shortcut-label .shortcut { --mdc-icon-size: var(--ha-space-3); display: inline-flex; flex-direction: row; align-items: center; gap: 2px; } .shortcut-label .shortcut span { font-size: var(--ha-font-size-s); font-family: var(--ha-font-family-code); color: var(--ha-color-text-secondary); } `, ]; } } declare global { interface HTMLElementTagNameMap { "add-automation-element-dialog": DialogAddAutomationElement; } }