diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts
index b1886a1844..2d69622da7 100644
--- a/src/components/target-picker/ha-target-picker-item-row.ts
+++ b/src/components/target-picker/ha-target-picker-item-row.ts
@@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
-import type { AreaRegistryEntry } from "../../data/area_registry";
+import type { AreaRegistryEntry } from "../../data/area/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
diff --git a/src/data/area/area_picker.ts b/src/data/area/area_picker.ts
new file mode 100644
index 0000000000..6c856d48f1
--- /dev/null
+++ b/src/data/area/area_picker.ts
@@ -0,0 +1,204 @@
+import { mdiTextureBox } from "@mdi/js";
+import { computeAreaName } from "../../common/entity/compute_area_name";
+import { computeDomain } from "../../common/entity/compute_domain";
+import { computeFloorName } from "../../common/entity/compute_floor_name";
+import { getAreaContext } from "../../common/entity/context/get_area_context";
+import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
+import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
+import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
+import type { HomeAssistant } from "../../types";
+import {
+ getDeviceEntityDisplayLookup,
+ type DeviceEntityDisplayLookup,
+ type DeviceRegistryEntry,
+} from "../device/device_registry";
+import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
+import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
+
+export const getAreas = (
+ haAreas: HomeAssistant["areas"],
+ haFloors: HomeAssistant["floors"],
+ haDevices: HomeAssistant["devices"],
+ haEntities: HomeAssistant["entities"],
+ haStates: HomeAssistant["states"],
+ includeDomains?: string[],
+ excludeDomains?: string[],
+ includeDeviceClasses?: string[],
+ deviceFilter?: HaDevicePickerDeviceFilterFunc,
+ entityFilter?: HaEntityPickerEntityFilterFunc,
+ excludeAreas?: string[],
+ idPrefix = ""
+): PickerComboBoxItem[] => {
+ let deviceEntityLookup: DeviceEntityDisplayLookup = {};
+ let inputDevices: DeviceRegistryEntry[] | undefined;
+ let inputEntities: EntityRegistryDisplayEntry[] | undefined;
+
+ const areas = Object.values(haAreas);
+ const devices = Object.values(haDevices);
+ const entities = Object.values(haEntities);
+
+ if (
+ includeDomains ||
+ excludeDomains ||
+ includeDeviceClasses ||
+ deviceFilter ||
+ entityFilter
+ ) {
+ deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
+ inputDevices = devices;
+ inputEntities = entities.filter((entity) => entity.area_id);
+
+ if (includeDomains) {
+ inputDevices = inputDevices!.filter((device) => {
+ const devEntities = deviceEntityLookup[device.id];
+ if (!devEntities || !devEntities.length) {
+ return false;
+ }
+ return deviceEntityLookup[device.id].some((entity) =>
+ includeDomains.includes(computeDomain(entity.entity_id))
+ );
+ });
+ inputEntities = inputEntities!.filter((entity) =>
+ includeDomains.includes(computeDomain(entity.entity_id))
+ );
+ }
+
+ if (excludeDomains) {
+ inputDevices = inputDevices!.filter((device) => {
+ const devEntities = deviceEntityLookup[device.id];
+ if (!devEntities || !devEntities.length) {
+ return true;
+ }
+ return entities.every(
+ (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
+ );
+ });
+ inputEntities = inputEntities!.filter(
+ (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
+ );
+ }
+
+ if (includeDeviceClasses) {
+ inputDevices = inputDevices!.filter((device) => {
+ const devEntities = deviceEntityLookup[device.id];
+ if (!devEntities || !devEntities.length) {
+ return false;
+ }
+ return deviceEntityLookup[device.id].some((entity) => {
+ const stateObj = haStates[entity.entity_id];
+ if (!stateObj) {
+ return false;
+ }
+ return (
+ stateObj.attributes.device_class &&
+ includeDeviceClasses.includes(stateObj.attributes.device_class)
+ );
+ });
+ });
+ inputEntities = inputEntities!.filter((entity) => {
+ const stateObj = haStates[entity.entity_id];
+ return (
+ stateObj.attributes.device_class &&
+ includeDeviceClasses.includes(stateObj.attributes.device_class)
+ );
+ });
+ }
+
+ if (deviceFilter) {
+ inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
+ }
+
+ if (entityFilter) {
+ inputDevices = inputDevices!.filter((device) => {
+ const devEntities = deviceEntityLookup[device.id];
+ if (!devEntities || !devEntities.length) {
+ return false;
+ }
+ return deviceEntityLookup[device.id].some((entity) => {
+ const stateObj = haStates[entity.entity_id];
+ if (!stateObj) {
+ return false;
+ }
+ return entityFilter(stateObj);
+ });
+ });
+ inputEntities = inputEntities!.filter((entity) => {
+ const stateObj = haStates[entity.entity_id];
+ if (!stateObj) {
+ return false;
+ }
+ return entityFilter!(stateObj);
+ });
+ }
+ }
+
+ let outputAreas = areas;
+
+ let areaIds: string[] | undefined;
+
+ if (inputDevices) {
+ areaIds = inputDevices
+ .filter((device) => device.area_id)
+ .map((device) => device.area_id!);
+ }
+
+ if (inputEntities) {
+ areaIds = (areaIds ?? []).concat(
+ inputEntities
+ .filter((entity) => entity.area_id)
+ .map((entity) => entity.area_id!)
+ );
+ }
+
+ if (areaIds) {
+ outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
+ }
+
+ if (excludeAreas) {
+ outputAreas = outputAreas.filter(
+ (area) => !excludeAreas!.includes(area.area_id)
+ );
+ }
+
+ const items = outputAreas.map
((area) => {
+ const { floor } = getAreaContext(area, haFloors);
+ const floorName = floor ? computeFloorName(floor) : undefined;
+ const areaName = computeAreaName(area);
+ return {
+ id: `${idPrefix}${area.area_id}`,
+ primary: areaName || area.area_id,
+ secondary: floorName,
+ icon: area.icon || undefined,
+ icon_path: area.icon ? undefined : mdiTextureBox,
+ search_labels: {
+ areaId: area.area_id,
+ aliases: area.aliases.join(" "),
+ },
+ };
+ });
+
+ return items;
+};
+
+export const areaComboBoxKeys: FuseWeightedKey[] = [
+ {
+ name: "primary",
+ weight: 10,
+ },
+ {
+ name: "search_labels.aliases",
+ weight: 8,
+ },
+ {
+ name: "secondary",
+ weight: 6,
+ },
+ {
+ name: "search_labels.domain",
+ weight: 4,
+ },
+ {
+ name: "search_labels.areaId",
+ weight: 2,
+ },
+];
diff --git a/src/data/area_registry.ts b/src/data/area/area_registry.ts
similarity index 90%
rename from src/data/area_registry.ts
rename to src/data/area/area_registry.ts
index e5174e0869..afa86d0f31 100644
--- a/src/data/area_registry.ts
+++ b/src/data/area/area_registry.ts
@@ -1,12 +1,12 @@
-import type { HomeAssistant } from "../types";
-import type { DeviceRegistryEntry } from "./device/device_registry";
+import type { HomeAssistant } from "../../types";
+import type { DeviceRegistryEntry } from "../device/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
-} from "./entity/entity_registry";
-import type { RegistryEntry } from "./registry";
+} from "../entity/entity_registry";
+import type { RegistryEntry } from "../registry";
-export { subscribeAreaRegistry } from "./ws-area_registry";
+export { subscribeAreaRegistry } from "../ws-area_registry";
export interface AreaRegistryEntry extends RegistryEntry {
aliases: string[];
diff --git a/src/data/area_floor_picker.ts b/src/data/area_floor_picker.ts
index 984ba51a41..61a4d68366 100644
--- a/src/data/area_floor_picker.ts
+++ b/src/data/area_floor_picker.ts
@@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
-import type { AreaRegistryEntry } from "./area_registry";
+import type { AreaRegistryEntry } from "./area/area_registry";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
diff --git a/src/data/floor_registry.ts b/src/data/floor_registry.ts
index 65cf336856..71a8d9f4fd 100644
--- a/src/data/floor_registry.ts
+++ b/src/data/floor_registry.ts
@@ -1,5 +1,5 @@
import type { HomeAssistant } from "../types";
-import type { AreaRegistryEntry } from "./area_registry";
+import type { AreaRegistryEntry } from "./area/area_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
diff --git a/src/data/quick_bar.ts b/src/data/quick_bar.ts
new file mode 100644
index 0000000000..edc3d5b849
--- /dev/null
+++ b/src/data/quick_bar.ts
@@ -0,0 +1,327 @@
+import {
+ mdiKeyboard,
+ mdiNavigationVariant,
+ mdiPuzzle,
+ mdiReload,
+ mdiServerNetwork,
+ mdiStorePlus,
+} from "@mdi/js";
+import { canShowPage } from "../common/config/can_show_page";
+import { componentsWithService } from "../common/config/components_with_service";
+import { isComponentLoaded } from "../common/config/is_component_loaded";
+import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
+import type { PageNavigation } from "../layouts/hass-tabs-subpage";
+import { configSections } from "../panels/config/ha-panel-config";
+import type { HomeAssistant } from "../types";
+import type { HassioAddonInfo } from "./hassio/addon";
+import { domainToName } from "./integration";
+import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
+import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
+
+export interface NavigationComboBoxItem extends PickerComboBoxItem {
+ path: string;
+ image?: string;
+ iconColor?: string;
+}
+
+export interface BaseNavigationCommand {
+ path: string;
+ primary: string;
+ icon_path?: string;
+ iconPath?: string;
+ iconColor?: string;
+ image?: string;
+}
+
+export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
+ action: string;
+ domain?: string;
+}
+
+export interface NavigationInfo extends PageNavigation {
+ primary: string;
+}
+
+const generateNavigationPanelCommands = (
+ localize: HomeAssistant["localize"],
+ panels: HomeAssistant["panels"],
+ addons?: HassioAddonInfo[]
+): BaseNavigationCommand[] =>
+ Object.entries(panels)
+ .filter(
+ ([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
+ )
+ .map(([_panelKey, panel]) => {
+ const translationKey = getPanelNameTranslationKey(panel);
+ const icon = getPanelIcon(panel) || "mdi:view-dashboard";
+
+ const primary = localize(translationKey) || panel.title || panel.url_path;
+
+ let image: string | undefined;
+
+ if (addons) {
+ const addon = addons.find(({ slug }) => slug === panel.url_path);
+ if (addon) {
+ image = addon.icon
+ ? `/api/hassio/addons/${addon.slug}/icon`
+ : undefined;
+ }
+ }
+
+ return {
+ primary,
+ icon,
+ image,
+ path: `/${panel.url_path}`,
+ };
+ });
+
+const getNavigationInfoFromConfig = (
+ localize: HomeAssistant["localize"],
+ page: PageNavigation
+): NavigationInfo | undefined => {
+ const path = page.path.substring(1);
+
+ let name = path.substring(path.indexOf("/") + 1);
+ name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
+
+ const caption =
+ (name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) ||
+ // @ts-expect-error
+ (page.translationKey && localize(page.translationKey));
+
+ if (caption) {
+ return { ...page, primary: caption };
+ }
+
+ return undefined;
+};
+
+const generateNavigationConfigSectionCommands = (
+ hass: HomeAssistant
+): BaseNavigationCommand[] => {
+ if (!hass.user?.is_admin) {
+ return [];
+ }
+
+ const items: NavigationInfo[] = [];
+
+ Object.values(configSections).forEach((sectionPages) => {
+ sectionPages.forEach((page) => {
+ if (!canShowPage(hass, page)) {
+ return;
+ }
+
+ const info = getNavigationInfoFromConfig(hass.localize, page);
+
+ if (!info) {
+ return;
+ }
+ // Add to list, but only if we do not already have an entry for the same path and component
+ if (items.some((e) => e.path === info.path)) {
+ return;
+ }
+
+ items.push(info);
+ });
+ });
+
+ return items;
+};
+
+const finalizeNavigationCommands = (
+ localize: HomeAssistant["localize"],
+ items: BaseNavigationCommand[]
+): NavigationComboBoxItem[] =>
+ items.map((item, index) => {
+ const secondary = localize(
+ "ui.dialogs.quick-bar.commands.types.navigation"
+ );
+ return {
+ id: `navigation_${index}_${item.path}`,
+ icon_path: item.iconPath || mdiNavigationVariant,
+ secondary,
+ sorting_label: `${item.primary}_${secondary}`,
+ ...item,
+ };
+ });
+
+export const generateNavigationCommands = (
+ hass: HomeAssistant,
+ addons?: HassioAddonInfo[]
+): NavigationComboBoxItem[] => {
+ const panelItems = generateNavigationPanelCommands(
+ hass.localize,
+ hass.panels,
+ addons
+ );
+ const sectionItems = generateNavigationConfigSectionCommands(hass);
+ const supervisorItems: BaseNavigationCommand[] = [];
+ if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
+ supervisorItems.push({
+ path: "/hassio/store",
+ icon_path: mdiStorePlus,
+ primary: hass.localize(
+ "ui.dialogs.quick-bar.commands.navigation.addon_store"
+ ),
+ });
+ supervisorItems.push({
+ path: "/hassio/dashboard",
+ icon_path: mdiPuzzle,
+ primary: hass.localize(
+ "ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
+ ),
+ });
+ if (addons) {
+ for (const addon of addons.filter((a) => a.version)) {
+ supervisorItems.push({
+ path: `/hassio/addon/${addon.slug}`,
+ image: addon.icon
+ ? `/api/hassio/addons/${addon.slug}/icon`
+ : undefined,
+ primary: hass.localize(
+ "ui.dialogs.quick-bar.commands.navigation.addon_info",
+ { addon: addon.name }
+ ),
+ });
+ }
+ }
+ }
+
+ const additionalItems = [
+ {
+ path: "",
+ primary: hass.localize(
+ "ui.dialogs.quick-bar.commands.navigation.shortcuts"
+ ),
+ icon_path: mdiKeyboard,
+ },
+ ];
+
+ return finalizeNavigationCommands(hass.localize, [
+ ...panelItems,
+ ...sectionItems,
+ ...supervisorItems,
+ ...additionalItems,
+ ]);
+};
+
+const generateReloadCommands = (
+ hass: HomeAssistant
+): ActionCommandComboBoxItem[] => {
+ // Get all domains that have a direct "reload" service
+ const reloadableDomains = componentsWithService(hass, "reload");
+
+ const commands = reloadableDomains.map((domain) => ({
+ primary:
+ hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
+ hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
+ domain: domainToName(hass.localize, domain),
+ }),
+ domain,
+ action: "reload",
+ icon_path: mdiReload,
+ secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`),
+ }));
+
+ // Add "frontend.reload_themes"
+ commands.push({
+ primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
+ domain: "frontend",
+ action: "reload_themes",
+ icon_path: mdiReload,
+ secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
+ });
+
+ // Add "homeassistant.reload_core_config"
+ commands.push({
+ primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
+ domain: "homeassistant",
+ action: "reload_core_config",
+ icon_path: mdiReload,
+ secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
+ });
+
+ // Add "homeassistant.reload_all"
+ commands.push({
+ primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"),
+ domain: "homeassistant",
+ action: "reload_all",
+ icon_path: mdiReload,
+ secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
+ });
+
+ return commands.map((command, index) => ({
+ ...command,
+ id: `command_${index}_${command.primary}`,
+ sorting_label: `${command.primary}_${command.secondary}_${command.domain}`,
+ }));
+};
+
+const generateServerControlCommands = (
+ hass: HomeAssistant
+): ActionCommandComboBoxItem[] => {
+ const serverActions = ["restart", "stop"] as const;
+
+ return serverActions.map((action, index) => {
+ const primary = hass.localize(
+ "ui.dialogs.quick-bar.commands.server_control.perform_action",
+ {
+ action: hass.localize(
+ `ui.dialogs.quick-bar.commands.server_control.${action}`
+ ),
+ }
+ );
+
+ const secondary = hass.localize(
+ "ui.dialogs.quick-bar.commands.types.server_control"
+ );
+
+ return {
+ id: `server_control_${index}_${action}`,
+ primary,
+ domain: "homeassistant",
+ icon_path: mdiServerNetwork,
+ secondary,
+ sorting_label: `${primary}_${secondary}_${action}`,
+ action,
+ };
+ });
+};
+
+export const generateActionCommands = (
+ hass: HomeAssistant
+): ActionCommandComboBoxItem[] => [
+ ...generateReloadCommands(hass),
+ ...generateServerControlCommands(hass),
+];
+
+export const commandComboBoxKeys: FuseWeightedKey[] = [
+ {
+ name: "primary",
+ weight: 10,
+ },
+ {
+ name: "domain",
+ weight: 8,
+ },
+ {
+ name: "secondary",
+ weight: 6,
+ },
+];
+
+export const navigateComboBoxKeys: FuseWeightedKey[] = [
+ {
+ name: "primary",
+ weight: 10,
+ },
+ {
+ name: "path",
+ weight: 8,
+ },
+ {
+ name: "secondary",
+ weight: 6,
+ },
+];
diff --git a/src/data/target.ts b/src/data/target.ts
index 25d3f741b1..975ba5d9c1 100644
--- a/src/data/target.ts
+++ b/src/data/target.ts
@@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
+import type { AreaRegistryEntry } from "./area/area_registry";
import type { FloorComboBoxItem } from "./area_floor_picker";
-import type { AreaRegistryEntry } from "./area_registry";
import type { DevicePickerItem } from "./device/device_picker";
import type { DeviceRegistryEntry } from "./device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
diff --git a/src/data/ws-area_registry.ts b/src/data/ws-area_registry.ts
index aaeee08392..a6113796b3 100644
--- a/src/data/ws-area_registry.ts
+++ b/src/data/ws-area_registry.ts
@@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { debounce } from "../common/util/debounce";
-import type { AreaRegistryEntry } from "./area_registry";
+import type { AreaRegistryEntry } from "./area/area_registry";
const fetchAreaRegistry = (conn: Connection) =>
conn.sendMessagePromise({
diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts
index 1c2fda3896..77298fe1b5 100644
--- a/src/dialogs/quick-bar/ha-quick-bar.ts
+++ b/src/dialogs/quick-bar/ha-quick-bar.ts
@@ -1,1140 +1,747 @@
-import type { ListItem } from "@material/mwc-list/mwc-list-item";
-import {
- mdiClose,
- mdiConsoleLine,
- mdiDevices,
- mdiEarth,
- mdiKeyboard,
- mdiMagnify,
- mdiReload,
- mdiServerNetwork,
-} from "@mdi/js";
+import { mdiDevices } from "@mdi/js";
import Fuse from "fuse.js";
-import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
-import { ifDefined } from "lit/directives/if-defined";
-import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
-import { canShowPage } from "../../common/config/can_show_page";
-import { componentsWithService } from "../../common/config/components_with_service";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
-import { computeAreaName } from "../../common/entity/compute_area_name";
-import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
-import { computeDomain } from "../../common/entity/compute_domain";
-import { entityUseDeviceName } from "../../common/entity/compute_entity_name";
-import { computeStateName } from "../../common/entity/compute_state_name";
-import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
-import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
-import { computeRTL } from "../../common/util/compute_rtl";
-import { debounce } from "../../common/util/debounce";
-import "../../components/ha-button";
-import "../../components/ha-icon-button";
-import "../../components/ha-label";
-import "../../components/ha-list";
-import "../../components/ha-md-list-item";
+import "../../components/entity/state-badge";
+import "../../components/ha-adaptive-dialog";
+import "../../components/ha-combo-box-item";
+import "../../components/ha-domain-icon";
+import "../../components/ha-icon";
+import "../../components/ha-picker-combo-box";
+import type {
+ HaPickerComboBox,
+ PickerComboBoxItem,
+} from "../../components/ha-picker-combo-box";
import "../../components/ha-spinner";
-import "../../components/ha-textfield";
+import "../../components/ha-svg-icon";
import "../../components/ha-tip";
-import { getConfigEntries } from "../../data/config_entries";
-import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
-import { domainToName } from "../../data/integration";
-import { getPanelNameTranslationKey } from "../../data/panel";
-import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
-import { configSections } from "../../panels/config/ha-panel-config";
-import { multiTermSortedSearch } from "../../resources/fuseMultiTerm";
+import { areaComboBoxKeys, getAreas } from "../../data/area/area_picker";
+import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import {
- haStyleDialog,
- haStyleDialogFixedTop,
- haStyleScrollbar,
-} from "../../resources/styles";
-import { loadVirtualizer } from "../../resources/virtualizer";
+ deviceComboBoxKeys,
+ getDevices,
+ type DevicePickerItem,
+} from "../../data/device/device_picker";
+import {
+ entityComboBoxKeys,
+ getEntities,
+ type EntityComboBoxItem,
+} from "../../data/entity/entity_picker";
+import {
+ fetchHassioAddonsInfo,
+ type HassioAddonInfo,
+} from "../../data/hassio/addon";
+import {
+ commandComboBoxKeys,
+ generateActionCommands,
+ generateNavigationCommands,
+ navigateComboBoxKeys,
+ type ActionCommandComboBoxItem,
+ type NavigationComboBoxItem,
+} from "../../data/quick_bar";
+import {
+ multiTermSortedSearch,
+ type FuseWeightedKey,
+} from "../../resources/fuseMultiTerm";
import type { HomeAssistant } from "../../types";
-import { brandsUrl } from "../../util/brands-url";
+import { isIosApp } from "../../util/is_ios";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
-import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
+import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar";
-const SEARCH_KEYS = [
- { name: "primaryText", weight: 10 },
- { name: "altText", weight: 8 },
- { name: "friendlyName", weight: 8 },
- { name: "area", weight: 6 },
- { name: "translatedDomain", weight: 5 },
- { name: "entityId", weight: 4 }, // for technical search
-];
-
-interface QuickBarItem extends ScorableTextItem {
- id: string;
- primaryText: string;
- iconPath?: string;
- action(data?: any): void;
-}
-
-interface CommandItem extends QuickBarItem {
- categoryKey: "reload" | "navigation" | "server_control";
- categoryText: string;
-}
-
-interface EntityItem extends QuickBarItem {
- altText: string;
- icon?: TemplateResult;
- translatedDomain: string;
- entityId: string;
- friendlyName: string;
-}
-
-interface DeviceItem extends QuickBarItem {
- deviceId: string;
- domain?: string;
- translatedDomain?: string;
- area?: string;
-}
-
-const isCommandItem = (item: QuickBarItem): item is CommandItem =>
- (item as CommandItem).categoryKey !== undefined;
-
-const isDeviceItem = (item: QuickBarItem): item is DeviceItem =>
- (item as DeviceItem).deviceId !== undefined;
-
-interface QuickBarNavigationItem extends CommandItem {
- path: string;
-}
-
-type NavigationInfo = PageNavigation & Pick;
-
-type BaseNavigationCommand = Pick<
- QuickBarNavigationItem,
- "primaryText" | "path"
->;
+const SEPARATOR = "________";
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @state() private _commandItems?: CommandItem[];
-
- @state() private _entityItems?: EntityItem[];
-
- @state() private _deviceItems?: DeviceItem[];
-
- @state() private _filter = "";
-
- @state() private _search = "";
-
@state() private _open = false;
- @state() private _opened = false;
-
- @state() private _narrow = false;
+ @state() private _loading = true;
@state() private _hint?: string;
- @state() private _mode = QuickBarMode.Entity;
+ @state() private _selectedSection?: QuickBarSection;
- @query("ha-textfield", false) private _filterInputField?: HTMLElement;
+ @state() private _opened = false;
- private _focusSet = false;
+ @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
- private _focusListElement?: ListItem | null;
+ private get _showEntityId() {
+ return this.hass.userData?.showEntityIdPicker;
+ }
+ private _configEntryLookup: Record = {};
+
+ private _addons?: HassioAddonInfo[];
+
+ private _translationsLoaded = false;
+
+ // #region lifecycle
public async showDialog(params: QuickBarParams) {
- this._mode = params.mode || QuickBarMode.Entity;
+ if (!this._translationsLoaded) {
+ this._fetchTranslations();
+ this._translationsLoaded = true;
+ }
+ this._initialize();
+ this._selectedSection = params.mode;
this._hint = params.hint;
- this._narrow = matchMedia(
- "all and (max-width: 450px), all and (max-height: 500px)"
- ).matches;
- this._initializeItemsIfNeeded();
this._open = true;
}
+ private async _fetchTranslations() {
+ await this.hass.loadBackendTranslation("title");
+ }
+
+ private async _initialize() {
+ try {
+ const configEntries = await getConfigEntries(this.hass);
+ this._configEntryLookup = Object.fromEntries(
+ configEntries.map((entry) => [entry.entry_id, entry])
+ );
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error("Error fetching config entries for quick bar", err);
+ }
+
+ if (this.hass.user?.is_admin && isComponentLoaded(this.hass, "hassio")) {
+ try {
+ const hassioAddonsInfo = await fetchHassioAddonsInfo(this.hass);
+ this._addons = hassioAddonsInfo.addons;
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error("Error fetching hassio addons for quick bar", err);
+ }
+ }
+
+ this._loading = false;
+ }
+
+ private _dialogOpened = async () => {
+ this._opened = true;
+ requestAnimationFrame(() => {
+ if (this.hass && isIosApp(this.hass)) {
+ this.hass.auth.external!.fireMessage({
+ type: "focus_element",
+ payload: {
+ element_id: "combo-box",
+ },
+ });
+ return;
+ }
+ this._comboBox?.focus();
+ });
+ };
+
+ // be sure to reload ha-picker-combo-box when adaptive-dialog mode changes
+ private _showTriggered = () => {
+ this._opened = false;
+ };
+
public closeDialog() {
this._open = false;
+ return true;
+ }
+
+ private _dialogClosed = () => {
+ this._selectedSection = undefined;
this._opened = false;
- this._focusSet = false;
- this._filter = "";
- this._search = "";
- this._entityItems = undefined;
- this._commandItems = undefined;
+ this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
- }
+ };
- protected willUpdate() {
- if (!this.hasUpdated) {
- loadVirtualizer();
- }
- }
+ // #endregion lifecycle
- private _getItems = memoizeOne(
- (
- mode: QuickBarMode,
- commandItems,
- entityItems,
- deviceItems,
- filter: string
- ) => {
- let items = entityItems;
-
- if (mode === QuickBarMode.Command) {
- items = commandItems;
- } else if (mode === QuickBarMode.Device) {
- items = deviceItems;
- }
-
- if (items && filter && filter !== " ") {
- return this._filterItems(items, filter);
- }
- return items;
- }
- );
+ // #region render
protected render() {
if (!this._open) {
return nothing;
}
- const items: QuickBarItem[] | undefined = this._getItems(
- this._mode,
- this._commandItems,
- this._entityItems,
- this._deviceItems,
- this._filter
- );
-
- const translationKey =
- this._mode === QuickBarMode.Device
- ? "filter_placeholder_devices"
- : "filter_placeholder";
- const placeholder = this.hass.localize(
- `ui.dialogs.quick-bar.${translationKey}`
- );
-
- const commandMode = this._mode === QuickBarMode.Command;
- const deviceMode = this._mode === QuickBarMode.Device;
- const icon = commandMode
- ? mdiConsoleLine
- : deviceMode
- ? mdiDevices
- : mdiMagnify;
- const searchPrefix = commandMode ? ">" : deviceMode ? "#" : "";
+ const sections = [
+ {
+ id: "navigate",
+ label: this.hass.localize("ui.dialogs.quick-bar.navigate_title"),
+ },
+ ...(this.hass.user?.is_admin
+ ? [
+ "separator" as const,
+ {
+ id: "command",
+ label: this.hass.localize("ui.dialogs.quick-bar.commands_title"),
+ },
+ ]
+ : []),
+ "separator" as const,
+ {
+ id: "entity",
+ label: this.hass.localize("ui.components.target-picker.type.entities"),
+ },
+ ...(this.hass.user?.is_admin
+ ? [
+ {
+ id: "device",
+ label: this.hass.localize(
+ "ui.components.target-picker.type.devices"
+ ),
+ },
+ {
+ id: "area",
+ label: this.hass.localize(
+ "ui.components.target-picker.type.areas"
+ ),
+ },
+ ]
+ : []),
+ ];
return html`
-
-
-
-
- ${this._search || this._narrow
- ? html`
-
- ${this._search &&
- html``}
- ${this._narrow
- ? html`
-
- ${this.hass!.localize("ui.common.close")}
-
- `
- : ""}
-
- `
- : ""}
-
-
- ${!items
- ? html``
- : items.length === 0
- ? html`
-
- ${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
-
- `
- : html`
-
- ${this._opened
- ? html`
- `
- : ""}
-
- `}
+ ${!this._loading && this._opened
+ ? html``
+ : nothing}
${this._hint
- ? html`${this._hint}`
- : ""}
-
+ ? html`${this._hint}`
+ : nothing}
+
`;
}
- private async _initializeItemsIfNeeded() {
- if (this._mode === QuickBarMode.Command) {
- this._commandItems =
- this._commandItems || (await this._generateCommandItems());
- } else if (this._mode === QuickBarMode.Device) {
- this._deviceItems =
- this._deviceItems || (await this._generateDeviceItems());
- } else {
- this._entityItems =
- this._entityItems || (await this._generateEntityItems());
- }
- }
-
- private _handleOpened() {
- this._opened = true;
- }
-
- private async _handleRangeChanged(e) {
- if (this._focusSet) {
- return;
- }
- if (e.firstVisible > -1) {
- this._focusSet = true;
- await this.updateComplete;
- this._setFocusFirstListItem();
- }
- }
-
- private _renderItem = (item: QuickBarItem, index: number) => {
+ private _renderRow = (
+ item:
+ | NavigationComboBoxItem
+ | ActionCommandComboBoxItem
+ | EntityComboBoxItem
+ | DevicePickerItem
+ ) => {
if (!item) {
return nothing;
}
- if (isDeviceItem(item)) {
- return this._renderDeviceItem(item, index);
- }
+ const iconPath = item.icon_path || mdiDevices;
- if (isCommandItem(item)) {
- return this._renderCommandItem(item, index);
- }
-
- return this._renderEntityItem(item as EntityItem, index);
+ return html`
+
+ ${"stateObj" in item && item.stateObj
+ ? html`
+
+ `
+ : "domain" in item && item.domain
+ ? html`
+
+ `
+ : "image" in item && item.image
+ ? html`
+
+ `
+ : item.icon
+ ? html``
+ : "iconColor" in item && item.iconColor
+ ? html`
+
+
+
+ `
+ : html`
+
+ `}
+ ${item.primary}
+ ${item.secondary
+ ? html`${item.secondary}`
+ : nothing}
+ ${"stateObj" in item && !!this._showEntityId
+ ? html`
+
+ ${item.stateObj?.entity_id}
+
+ `
+ : nothing}
+ ${"domain_name" in item &&
+ (!("stateObj" in item) || !this._showEntityId)
+ ? html`
+
+ ${(item as EntityComboBoxItem).domain_name}
+
+ `
+ : nothing}
+
+ `;
};
- private _renderDeviceItem(item: DeviceItem, index?: number) {
- return html`
-
- ${item.domain
- ? html`
`
- : nothing}
- ${item.primaryText}
- ${item.area
- ? html` ${item.area} `
- : nothing}
- ${item.translatedDomain
- ? html`
- ${item.translatedDomain}
-
`
- : nothing}
-
- `;
- }
-
- private _renderEntityItem(item: EntityItem, index?: number) {
- const showEntityId = this.hass.userData?.showEntityIdPicker;
-
- return html`
-
- ${item.iconPath
- ? html`
-
- `
- : html`${item.icon}`}
- ${item.primaryText}
- ${item.altText
- ? html` ${item.altText} `
- : nothing}
- ${item.entityId && showEntityId
- ? html`
- ${item.entityId}
- `
- : nothing}
- ${item.translatedDomain && !showEntityId
- ? html`
- ${item.translatedDomain}
-
`
- : nothing}
-
- `;
- }
-
- private _renderCommandItem(item: CommandItem, index?: number) {
- return html`
-
-
-
- ${item.iconPath
- ? html`
-
- `
- : nothing}
- ${item.categoryText}
-
-
-
- ${item.primaryText}
-
- `;
- }
-
- private async _processItemAndCloseDialog(item: QuickBarItem, index: number) {
- this._addSpinnerToCommandItem(index);
-
- await item.action();
- this.closeDialog();
- }
-
- private _handleInputKeyDown(ev: KeyboardEvent) {
- if (ev.code === "Enter") {
- const firstItem = this._getItemAtIndex(0);
- if (!firstItem || firstItem.style.display === "none") {
- return;
- }
- this._processItemAndCloseDialog((firstItem as any).item, 0);
- } else if (ev.code === "ArrowDown") {
- ev.preventDefault();
- this._getItemAtIndex(0)?.focus();
- this._focusSet = true;
- this._focusListElement = this._getItemAtIndex(0);
- }
- }
-
- private _getItemAtIndex(index: number): ListItem | null {
- return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`);
- }
-
- private _addSpinnerToCommandItem(index: number): void {
- const div = document.createElement("div");
- div.slot = "meta";
+ private _getRowSpinner = memoizeOne(() => {
const spinner = document.createElement("ha-spinner");
spinner.size = "small";
- div.appendChild(spinner);
- this._getItemAtIndex(index)?.appendChild(div);
- }
+ spinner.style.marginRight = "16px";
+ spinner.style.position = "absolute";
+ spinner.style.right = "0";
+ return spinner;
+ });
- private _handleSearchChange(ev: CustomEvent): void {
- const newFilter = (ev.currentTarget as any).value;
- const oldMode = this._mode;
- const oldSearch = this._search;
- let newMode: QuickBarMode;
- let newSearch: string;
-
- if (newFilter.startsWith(">")) {
- newMode = QuickBarMode.Command;
- newSearch = newFilter.substring(1);
- } else if (newFilter.startsWith("#")) {
- newMode = QuickBarMode.Device;
- newSearch = newFilter.substring(1);
- } else {
- newMode = QuickBarMode.Entity;
- newSearch = newFilter;
+ private _sectionTitleFunction = ({
+ firstIndex,
+ lastIndex,
+ firstItem,
+ secondItem,
+ itemsCount,
+ }: {
+ firstIndex: number;
+ lastIndex: number;
+ firstItem: PickerComboBoxItem | string;
+ secondItem: PickerComboBoxItem | string;
+ itemsCount: number;
+ }) => {
+ if (
+ firstItem === undefined ||
+ secondItem === undefined ||
+ typeof firstItem === "string" ||
+ (typeof secondItem === "string" && secondItem !== "padding") ||
+ (firstIndex === 0 && lastIndex === itemsCount - 1)
+ ) {
+ return undefined;
}
- if (oldMode === newMode && oldSearch === newSearch) {
- return;
- }
+ const type =
+ "action" in firstItem
+ ? this.hass.localize("ui.dialogs.quick-bar.commands_title")
+ : "path" in firstItem
+ ? this.hass.localize("ui.dialogs.quick-bar.navigate_title")
+ : "stateObj" in firstItem
+ ? this.hass.localize("ui.components.target-picker.type.entities")
+ : "domain" in firstItem
+ ? this.hass.localize("ui.components.target-picker.type.devices")
+ : this.hass.localize("ui.components.target-picker.type.areas");
- this._mode = newMode;
- this._search = newSearch;
+ return type;
+ };
- if (this._hint) {
- this._hint = undefined;
- }
+ // #endregion render
- if (oldMode !== this._mode) {
- this._focusSet = false;
- this._initializeItemsIfNeeded();
- this._filter = this._search;
- } else {
- if (this._focusSet && this._focusListElement) {
- this._focusSet = false;
- // @ts-ignore
- this._focusListElement.rippleHandlers.endFocus();
- }
- this._debouncedSetFilter(this._search);
- }
- }
+ // #region data
- private _clearSearch() {
- this._search = "";
- this._filter = "";
- }
-
- private _debouncedSetFilter = debounce((filter: string) => {
- this._filter = filter;
- }, 100);
-
- private _setFocusFirstListItem() {
- // @ts-ignore
- this._getItemAtIndex(0)?.rippleHandlers.startFocus();
- this._focusListElement = this._getItemAtIndex(0);
- }
-
- private _handleListItemKeyDown(ev: KeyboardEvent) {
- const isSingleCharacter = ev.key.length === 1;
- const index = (ev.target as HTMLElement).getAttribute("index");
- const isFirstListItem = index === "0";
- this._focusListElement = ev.target as ListItem;
- if (ev.key === "ArrowDown") {
- this._getItemAtIndex(Number(index) + 1)?.focus();
- }
- if (ev.key === "ArrowUp") {
- if (isFirstListItem) {
- this._filterInputField?.focus();
- } else {
- this._getItemAtIndex(Number(index) - 1)?.focus();
- }
- }
- if (ev.key === "Enter" || ev.key === " ") {
- this._processItemAndCloseDialog(
- (ev.target as any).item,
- Number((ev.target as HTMLElement).getAttribute("index"))
- );
- }
- if (ev.key === "Backspace" || isSingleCharacter) {
- (ev.currentTarget as HTMLElement).scrollTop = 0;
- this._filterInputField?.focus();
- }
- }
-
- private _handleItemClick(ev) {
- const listItem = ev.target.closest("ha-md-list-item");
- this._processItemAndCloseDialog(
- listItem.item,
- Number(listItem.getAttribute("index"))
+ private _getItems = (searchString: string, section: string) => {
+ this._selectedSection = section as QuickBarSection | undefined;
+ return this._getItemsMemoized(
+ this._configEntryLookup,
+ searchString,
+ this._selectedSection
);
- }
+ };
- private async _generateDeviceItems(): Promise {
- const configEntries = await getConfigEntries(this.hass);
- const configEntryLookup = Object.fromEntries(
- configEntries.map((entry) => [entry.entry_id, entry])
- );
+ private _getItemsMemoized = memoizeOne(
+ (
+ configEntryLookup: Record,
+ filter?: string,
+ section?: QuickBarSection
+ ) => {
+ const items: (string | PickerComboBoxItem)[] = [];
- return Object.values(this.hass.devices)
- .filter((device) => !device.disabled_by)
- .map((device) => {
- const deviceName = computeDeviceNameDisplay(device, this.hass);
+ if (!section || section === "navigate") {
+ let navigateItems = this._generateNavigationCommandsMemoized(
+ this.hass,
+ this._addons
+ ).sort(this._sortBySortingLabel);
- const { area } = getDeviceContext(device, this.hass);
+ if (filter) {
+ navigateItems = this._filterGroup(
+ "navigate",
+ navigateItems,
+ filter,
+ navigateComboBoxKeys
+ ) as NavigationComboBoxItem[];
+ }
- const areaName = area ? computeAreaName(area) : undefined;
+ if (!section && navigateItems.length) {
+ // show group title
+ items.push(this.hass.localize("ui.dialogs.quick-bar.navigate_title"));
+ }
- const deviceItem = {
- id: device.id,
- primaryText: deviceName,
- deviceId: device.id,
- area: areaName,
- action: () => navigate(`/config/devices/device/${device.id}`),
- };
+ items.push(...navigateItems);
+ }
- const configEntry = device.primary_config_entry
- ? configEntryLookup[device.primary_config_entry]
- : undefined;
-
- const domain = configEntry?.domain;
- const translatedDomain = domain
- ? domainToName(this.hass.localize, domain)
- : undefined;
-
- return {
- ...deviceItem,
- domain,
- translatedDomain,
- strings: [deviceName, areaName, domain, domainToName].filter(
- Boolean
- ) as string[],
- };
- })
- .sort((a, b) =>
- caseInsensitiveStringCompare(
- a.primaryText,
- b.primaryText,
- this.hass.locale.language
- )
- );
- }
-
- private async _generateEntityItems(): Promise {
- const isRTL = computeRTL(this.hass);
-
- await this.hass.loadBackendTranslation("title");
-
- return Object.keys(this.hass.states)
- .map((entityId) => {
- const stateObj = this.hass.states[entityId];
-
- const friendlyName = computeStateName(stateObj); // Keep this for search
-
- const useDeviceName = entityUseDeviceName(
- stateObj,
- this.hass.entities,
- this.hass.devices
+ if (this.hass.user?.is_admin && (!section || section === "command")) {
+ let commandItems = this._generateActionCommandsMemoized(this.hass).sort(
+ this._sortBySortingLabel
);
- const name = this.hass.formatEntityName(
- stateObj,
- useDeviceName ? { type: "device" } : { type: "entity" }
+ if (filter) {
+ commandItems = this._filterGroup(
+ "command",
+ commandItems,
+ filter,
+ commandComboBoxKeys
+ ) as ActionCommandComboBoxItem[];
+ }
+
+ if (!section && commandItems.length) {
+ // show group title
+ items.push(this.hass.localize("ui.dialogs.quick-bar.commands_title"));
+ }
+
+ items.push(...commandItems);
+ }
+
+ if (!section || section === "entity") {
+ let entityItems = this._getEntitiesMemoized(this.hass).sort(
+ this._sortBySortingLabel
);
- const primary = name || entityId;
+ if (filter) {
+ entityItems = this._filterGroup(
+ "entity",
+ entityItems,
+ filter,
+ entityComboBoxKeys
+ ) as EntityComboBoxItem[];
+ }
- const secondary = this.hass.formatEntityName(
- stateObj,
- useDeviceName
- ? [{ type: "area" }]
- : [{ type: "area" }, { type: "device" }],
- {
- separator: isRTL ? " ◂ " : " ▸ ",
- }
- );
+ if (!section && entityItems.length) {
+ // show group title
+ items.push(
+ this.hass.localize("ui.components.target-picker.type.entities")
+ );
+ }
- const translatedDomain = domainToName(
- this.hass.localize,
- computeDomain(entityId)
- );
+ items.push(...entityItems);
+ }
- const entityItem = {
- id: `entity-${entityId}`,
- primaryText: primary,
- altText: secondary,
- icon: html`
-
- `,
- translatedDomain: translatedDomain,
- entityId: entityId,
- friendlyName: friendlyName,
- action: () => fireEvent(this, "hass-more-info", { entityId }),
- };
+ if (this.hass.user?.is_admin && (!section || section === "device")) {
+ let deviceItems = this._getDevicesMemoized(
+ this.hass,
+ configEntryLookup
+ ).sort(this._sortBySortingLabel);
- return {
- ...entityItem,
- strings: [entityItem.primaryText, entityItem.altText],
- };
- })
- .sort((a, b) =>
- caseInsensitiveStringCompare(
- a.primaryText,
- b.primaryText,
- this.hass.locale.language
- )
- );
- }
+ if (filter) {
+ deviceItems = this._filterGroup(
+ "device",
+ deviceItems,
+ filter,
+ deviceComboBoxKeys
+ );
+ }
- private async _generateCommandItems(): Promise {
- return [
- ...(await this._generateReloadCommands()),
- ...this._generateServerControlCommands(),
- ...(await this._generateNavigationCommands()),
- ].sort((a, b) =>
- caseInsensitiveStringCompare(
- a.strings.join(" "),
- b.strings.join(" "),
- this.hass.locale.language
+ if (!section && deviceItems.length) {
+ // show group title
+ items.push(
+ this.hass.localize("ui.components.target-picker.type.devices")
+ );
+ }
+
+ items.push(...deviceItems);
+ }
+
+ if (this.hass.user?.is_admin && (!section || section === "area")) {
+ let areaItems = this._getAreasMemoized(this.hass);
+
+ if (filter) {
+ areaItems = this._filterGroup(
+ "area",
+ areaItems,
+ filter,
+ areaComboBoxKeys
+ );
+ }
+
+ if (!section && areaItems.length) {
+ // show group title
+ items.push(
+ this.hass.localize("ui.components.target-picker.type.areas")
+ );
+ }
+
+ items.push(...areaItems);
+ }
+
+ return items;
+ }
+ );
+
+ private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
+ getEntities(
+ hass,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ `entity${SEPARATOR}`
+ )
+ );
+
+ private _getDevicesMemoized = memoizeOne(
+ (hass: HomeAssistant, configEntryLookup: Record) =>
+ getDevices(
+ hass,
+ configEntryLookup,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ `device${SEPARATOR}`
)
+ );
+
+ private _getAreasMemoized = memoizeOne((hass: HomeAssistant) =>
+ getAreas(
+ hass.areas,
+ hass.floors,
+ hass.devices,
+ hass.entities,
+ hass.states,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ `area${SEPARATOR}`
+ )
+ );
+
+ private _generateNavigationCommandsMemoized = memoizeOne(
+ generateNavigationCommands
+ );
+
+ private _generateActionCommandsMemoized = memoizeOne(generateActionCommands);
+
+ private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
+ Fuse.createIndex(keys, states);
+
+ private _fuseIndexes = {
+ entity: memoizeOne((states: PickerComboBoxItem[]) =>
+ this._createFuseIndex(states, entityComboBoxKeys)
+ ),
+ device: memoizeOne((states: PickerComboBoxItem[]) =>
+ this._createFuseIndex(states, deviceComboBoxKeys)
+ ),
+ area: memoizeOne((states: PickerComboBoxItem[]) =>
+ this._createFuseIndex(states, areaComboBoxKeys)
+ ),
+ command: memoizeOne((states: PickerComboBoxItem[]) =>
+ this._createFuseIndex(states, commandComboBoxKeys)
+ ),
+ navigate: memoizeOne((states: PickerComboBoxItem[]) =>
+ this._createFuseIndex(states, navigateComboBoxKeys)
+ ),
+ };
+
+ private _filterGroup(
+ type: QuickBarSection,
+ items: PickerComboBoxItem[],
+ searchTerm: string,
+ weightedKeys: FuseWeightedKey[]
+ ) {
+ const fuseIndex = this._fuseIndexes[type](items);
+
+ return multiTermSortedSearch(
+ items,
+ searchTerm,
+ weightedKeys,
+ (item: PickerComboBoxItem) => item.id,
+ fuseIndex
);
}
- private async _generateReloadCommands(): Promise {
- // Get all domains that have a direct "reload" service
- const reloadableDomains = componentsWithService(this.hass, "reload");
-
- const localize = await this.hass.loadBackendTranslation(
- "title",
- reloadableDomains
+ private _sortBySortingLabel = (entityA, entityB) =>
+ caseInsensitiveStringCompare(
+ (entityA as PickerComboBoxItem).sorting_label!,
+ (entityB as PickerComboBoxItem).sorting_label!,
+ this.hass.locale.language
);
- const commands = reloadableDomains.map((domain) => ({
- primaryText:
- this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
- this.hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
- domain: domainToName(localize, domain),
- }),
- action: () => this.hass.callService(domain, "reload"),
- iconPath: mdiReload,
- categoryText: this.hass.localize(
- `ui.dialogs.quick-bar.commands.types.reload`
- ),
- }));
+ // #endregion data
- // Add "frontend.reload_themes"
- commands.push({
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.reload.themes"
- ),
- action: () => this.hass.callService("frontend", "reload_themes"),
- iconPath: mdiReload,
- categoryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.types.reload"
- ),
- });
+ // #region interaction
- // Add "homeassistant.reload_core_config"
- commands.push({
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.reload.core"
- ),
- action: () =>
- this.hass.callService("homeassistant", "reload_core_config"),
- iconPath: mdiReload,
- categoryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.types.reload"
- ),
- });
+ private async _handleItemSelected(ev: CustomEvent<{ index: number }>) {
+ if (this._comboBox && this._comboBox.virtualizerElement) {
+ const index = ev.detail.index;
+ const item = this._comboBox.virtualizerElement.items[
+ index
+ ] as PickerComboBoxItem;
- // Add "homeassistant.reload_all"
- commands.push({
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.reload.all"
- ),
- action: () => this.hass.callService("homeassistant", "reload_all"),
- iconPath: mdiReload,
- categoryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.types.reload"
- ),
- });
+ // entity selected
+ if (item && "stateObj" in item) {
+ this.closeDialog();
+ fireEvent(this, "hass-more-info", {
+ entityId: item.search_labels!.entityId,
+ });
+ return;
+ }
- return commands.map((command, index) => ({
- ...command,
- id: `command_${index}_${command.primaryText}`,
- categoryKey: "reload",
- strings: [`${command.categoryText} ${command.primaryText}`],
- }));
- }
+ // device selected
+ if (item && item.id.startsWith(`device${SEPARATOR}`)) {
+ this.closeDialog();
+ navigate(`/config/devices/device/${item.id.split(SEPARATOR)[1]}`);
+ return;
+ }
- private _generateServerControlCommands(): CommandItem[] {
- const serverActions = ["restart", "stop"] as const;
+ // area selected
+ if (item && item.id.startsWith(`area${SEPARATOR}`)) {
+ this.closeDialog();
+ navigate(`/config/areas/area/${item.id.split(SEPARATOR)[1]}`);
+ return;
+ }
- return serverActions.map((action, index) => {
- const categoryKey: CommandItem["categoryKey"] = "server_control";
-
- const item = {
- id: `server_control_${index}_${action}`,
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.server_control.perform_action",
- {
- action: this.hass.localize(
- `ui.dialogs.quick-bar.commands.server_control.${action}`
- ),
- }
- ),
- iconPath: mdiServerNetwork,
- categoryText: this.hass.localize(
- `ui.dialogs.quick-bar.commands.types.${categoryKey}`
- ),
- categoryKey,
- action: async () => {
+ // command selected
+ if (item && "action" in item) {
+ const actionItem = item as ActionCommandComboBoxItem;
+ if (actionItem.action === "restart" || actionItem.action === "stop") {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
- `ui.dialogs.restart.${action}.confirm_title`
+ `ui.dialogs.restart.${actionItem.action}.confirm_title`
),
text: this.hass.localize(
- `ui.dialogs.restart.${action}.confirm_description`
+ `ui.dialogs.restart.${actionItem.action}.confirm_description`
),
confirmText: this.hass.localize(
- `ui.dialogs.restart.${action}.confirm_action`
+ `ui.dialogs.restart.${actionItem.action}.confirm_action`
),
destructive: true,
});
if (!confirmed) {
return;
}
- this.hass.callService("homeassistant", action);
- },
- };
- return {
- ...item,
- strings: [`${item.categoryText} ${item.primaryText}`],
- };
- });
- }
+ this.hass.callService(actionItem.domain!, actionItem.action);
+ this.closeDialog();
+ return;
+ }
- private async _generateNavigationCommands(): Promise {
- const panelItems = this._generateNavigationPanelCommands();
- const sectionItems = this._generateNavigationConfigSectionCommands();
- const supervisorItems: BaseNavigationCommand[] = [];
- if (isComponentLoaded(this.hass, "hassio")) {
- const addonsInfo = await fetchHassioAddonsInfo(this.hass);
- supervisorItems.push({
- path: "/hassio/store",
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.navigation.addon_store"
- ),
- });
- supervisorItems.push({
- path: "/hassio/dashboard",
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
- ),
- });
- for (const addon of addonsInfo.addons.filter((a) => a.version)) {
- supervisorItems.push({
- path: `/hassio/addon/${addon.slug}`,
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.navigation.addon_info",
- { addon: addon.name }
- ),
- });
+ const element = this._comboBox.virtualizerElement.querySelector(
+ `#list-item-${index}`
+ ) as HTMLDivElement | null;
+
+ if (element) {
+ element.style.backgroundColor =
+ "var(--ha-color-fill-primary-normal-resting)";
+ element.prepend(this._getRowSpinner());
+ }
+
+ await this.hass.callService(actionItem.domain!, actionItem.action);
+
+ this.closeDialog();
+ return;
+ }
+
+ // navigation selected
+ if (item && "path" in item) {
+ this.closeDialog();
+
+ if (!item.path) {
+ showShortcutsDialog(this);
+ return;
+ }
+
+ navigate((item as NavigationComboBoxItem).path);
}
}
-
- const additionalItems = [
- {
- path: "",
- primaryText: this.hass.localize(
- "ui.dialogs.quick-bar.commands.navigation.shortcuts"
- ),
- action: () => showShortcutsDialog(this),
- iconPath: mdiKeyboard,
- },
- ];
-
- return this._finalizeNavigationCommands([
- ...panelItems,
- ...sectionItems,
- ...supervisorItems,
- ...additionalItems,
- ]);
}
- private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
- return Object.keys(this.hass.panels)
- .filter(
- (panelKey) => panelKey !== "_my_redirect" && panelKey !== "hassio"
- )
- .map((panelKey) => {
- const panel = this.hass.panels[panelKey];
- const translationKey = getPanelNameTranslationKey(panel);
+ // #endregion interaction
- const primaryText =
- this.hass.localize(translationKey) || panel.title || panel.url_path;
+ // #region styles
- return {
- primaryText,
- path: `/${panel.url_path}`,
- };
- });
- }
-
- private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
- const items: NavigationInfo[] = [];
-
- for (const sectionKey of Object.keys(configSections)) {
- for (const page of configSections[sectionKey]) {
- if (!canShowPage(this.hass, page)) {
- continue;
- }
-
- const info = this._getNavigationInfoFromConfig(page);
-
- if (!info) {
- continue;
- }
- // Add to list, but only if we do not already have an entry for the same path and component
- if (items.some((e) => e.path === info.path)) {
- continue;
- }
-
- items.push(info);
- }
- }
-
- return items;
- }
-
- private _getNavigationInfoFromConfig(
- page: PageNavigation
- ): NavigationInfo | undefined {
- const path = page.path.substring(1);
-
- let name = path.substring(path.indexOf("/") + 1);
- name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
-
- const caption =
- (name &&
- this.hass.localize(
- `ui.dialogs.quick-bar.commands.navigation.${name}`
- )) ||
- // @ts-expect-error
- (page.translationKey && this.hass.localize(page.translationKey));
-
- if (caption) {
- return { ...page, primaryText: caption };
- }
-
- return undefined;
- }
-
- private _finalizeNavigationCommands(
- items: BaseNavigationCommand[]
- ): CommandItem[] {
- return items.map((item, index) => {
- const categoryKey: CommandItem["categoryKey"] = "navigation";
-
- const navItem = {
- id: `navigation_${index}_${item.path}`,
- iconPath: mdiEarth,
- categoryText: this.hass.localize(
- `ui.dialogs.quick-bar.commands.types.${categoryKey}`
- ),
- action: () => navigate(item.path),
- ...item,
- };
-
- return {
- ...navItem,
- strings: [`${navItem.categoryText} ${navItem.primaryText}`],
- categoryKey,
- };
- });
- }
-
- private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
- Fuse.createIndex(SEARCH_KEYS, items)
- );
-
- private _filterItems = memoizeOne(
- (items: QuickBarItem[], filter: string): QuickBarItem[] => {
- const index = this._fuseIndex(items);
-
- return multiTermSortedSearch(
- items,
- filter,
- SEARCH_KEYS,
- (item) => item.id,
- index
+ static styles = css`
+ :host {
+ --dialog-surface-margin-top: var(--ha-space-10);
+ --ha-dialog-min-height: 620px;
+ --ha-bottom-sheet-height: calc(
+ 100vh - max(var(--safe-area-inset-top), 48px)
);
+ --ha-bottom-sheet-height: calc(
+ 100dvh - max(var(--safe-area-inset-top), 48px)
+ );
+ --ha-bottom-sheet-max-height: calc(
+ 100vh - max(var(--safe-area-inset-top), 48px)
+ );
+ --ha-bottom-sheet-max-height: calc(
+ 100dvh - max(var(--safe-area-inset-top), 48px)
+ );
+ --dialog-content-padding: 0;
+ --safe-area-inset-bottom: 0px;
}
- );
- static get styles() {
- return [
- haStyleScrollbar,
- haStyleDialog,
- haStyleDialogFixedTop,
- css`
- ha-list {
- position: relative;
- --mdc-list-vertical-padding: 0;
- }
- .heading {
- display: flex;
- align-items: center;
- --mdc-theme-primary: var(--primary-text-color);
- }
+ ha-tip {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: var(--secondary-text-color);
+ gap: var(--ha-space-1);
+ }
- .heading ha-textfield {
- flex-grow: 1;
- }
+ ha-tip a {
+ color: var(--primary-color);
+ }
- ha-dialog {
- --dialog-z-index: 9;
- --dialog-content-padding: 0;
- }
+ @media all and (max-width: 450px), all and (max-height: 690px) {
+ ha-tip {
+ display: none;
+ }
+ }
+ `;
- @media (min-width: 800px) {
- ha-dialog {
- --mdc-dialog-max-width: 800px;
- --mdc-dialog-min-width: 500px;
- --mdc-dialog-max-height: calc(
- 100vh - var(--ha-space-18) - var(--safe-area-inset-y)
- );
- }
- }
-
- @media all and (max-width: 450px), all and (max-height: 500px) {
- ha-textfield {
- --mdc-shape-small: 0;
- }
- }
-
- @media all and (max-width: 450px), all and (max-height: 690px) {
- .hint {
- display: none;
- }
- }
-
- ha-svg-icon.prefix {
- color: var(--primary-text-color);
- }
-
- ha-textfield ha-icon-button {
- --mdc-icon-button-size: 24px;
- color: var(--primary-text-color);
- }
-
- .command-category {
- --ha-label-icon-color: #585858;
- --ha-label-text-color: #212121;
- }
-
- .command-category.reload {
- --ha-label-background-color: #cddc39;
- }
-
- .command-category.navigation {
- --ha-label-background-color: var(--light-primary-color);
- }
-
- .command-category.server_control {
- --ha-label-background-color: var(--warning-color);
- }
-
- span.command-text {
- margin-left: var(--ha-space-2);
- margin-inline-start: var(--ha-space-2);
- margin-inline-end: initial;
- direction: var(--direction);
- }
-
- ha-md-list-item {
- width: 100%;
- }
-
- /* Fixed height for items because we are use virtualizer */
- 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);
- }
-
- ha-md-list-item.three-line {
- width: 100%;
- --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);
- }
-
- ha-md-list-item .code {
- font-family: var(--ha-font-family-code);
- font-size: var(--ha-font-size-xs);
- }
-
- ha-md-list-item .domain {
- font-size: var(--ha-font-size-s);
- font-weight: var(--ha-font-weight-normal);
- line-height: var(--ha-line-height-normal);
- align-self: flex-end;
- max-width: 30%;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
-
- ha-md-list-item img {
- width: 32px;
- height: 32px;
- }
-
- ha-tip {
- padding: var(--ha-space-5);
- }
-
- .nothing-found {
- padding: var(--ha-space-4) 0;
- text-align: center;
- }
-
- div[slot="trailingIcon"] {
- display: flex;
- align-items: center;
- }
-
- lit-virtualizer {
- contain: size layout !important;
- }
- `,
- ];
- }
+ // #endregion styles
}
declare global {
diff --git a/src/dialogs/quick-bar/show-dialog-quick-bar.ts b/src/dialogs/quick-bar/show-dialog-quick-bar.ts
index 2609ac8b35..b83818ad9d 100644
--- a/src/dialogs/quick-bar/show-dialog-quick-bar.ts
+++ b/src/dialogs/quick-bar/show-dialog-quick-bar.ts
@@ -1,14 +1,15 @@
import { fireEvent } from "../../common/dom/fire_event";
-export const enum QuickBarMode {
- Command = "command",
- Device = "device",
- Entity = "entity",
-}
+export type QuickBarSection =
+ | "entity"
+ | "device"
+ | "area"
+ | "navigate"
+ | "command";
export interface QuickBarParams {
entityFilter?: string;
- mode?: QuickBarMode;
+ mode?: QuickBarSection;
hint?: string;
}
diff --git a/src/dialogs/shortcuts/dialog-shortcuts.ts b/src/dialogs/shortcuts/dialog-shortcuts.ts
index ced2f6e05a..4e75a02f00 100644
--- a/src/dialogs/shortcuts/dialog-shortcuts.ts
+++ b/src/dialogs/shortcuts/dialog-shortcuts.ts
@@ -1,11 +1,10 @@
-import { css, html, LitElement, nothing } from "lit";
+import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
-import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-svg-icon";
-import { haStyleDialog } from "../../resources/styles";
+import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
@@ -38,6 +37,10 @@ const _SHORTCUTS: Section[] = [
{
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
},
+ {
+ shortcut: [CTRL_CMD, "K"],
+ descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search",
+ },
{
shortcut: ["C"],
descriptionTranslationKey:
@@ -165,17 +168,22 @@ const _SHORTCUTS: Section[] = [
class DialogShortcuts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @state() private _opened = false;
+ @state() private _open = false;
public async showDialog(): Promise {
- this._opened = true;
+ this._open = true;
}
- public async closeDialog(): Promise {
- this._opened = false;
+ private _dialogClosed() {
+ this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
+ public async closeDialog() {
+ this._open = false;
+ return true;
+ }
+
private _renderShortcut(
shortcutKeys: ShortcutString[],
descriptionKey: LocalizeKeys
@@ -202,20 +210,11 @@ class DialogShortcuts extends LitElement {
}
protected render() {
- if (!this._opened) {
- return nothing;
- }
-
return html`
-
${_SHORTCUTS.map(
@@ -238,7 +237,7 @@ class DialogShortcuts extends LitElement {
)}
-
+
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`${this.hass.localize(
@@ -247,25 +246,12 @@ class DialogShortcuts extends LitElement {
>`,
})}
-
+
`;
}
static styles = [
- haStyleDialog,
css`
- ha-dialog {
- --dialog-z-index: 15;
- }
-
- h3:first-of-type {
- margin-top: 0;
- }
-
- .content {
- margin-bottom: 24px;
- }
-
.shortcut {
display: flex;
flex-direction: row;
@@ -287,6 +273,10 @@ class DialogShortcuts extends LitElement {
ha-svg-icon {
width: 12px;
}
+
+ ha-alert a {
+ color: var(--primary-color);
+ }
`,
];
}
diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts
index 6071e66e15..60ff671fe7 100644
--- a/src/panels/config/areas/dialog-area-registry-detail.ts
+++ b/src/panels/config/areas/dialog-area-registry-detail.ts
@@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
-} from "../../../data/area_registry";
-import { deleteAreaRegistryEntry } from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
+import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
import {
SENSOR_DEVICE_CLASS_HUMIDITY,
SENSOR_DEVICE_CLASS_TEMPERATURE,
diff --git a/src/panels/config/areas/dialog-areas-floors-order.ts b/src/panels/config/areas/dialog-areas-floors-order.ts
index b0eec9ca6e..c4b5278d1e 100644
--- a/src/panels/config/areas/dialog-areas-floors-order.ts
+++ b/src/panels/config/areas/dialog-areas-floors-order.ts
@@ -14,16 +14,16 @@ import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
-import "../../../components/ha-wa-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
-import type { AreaRegistryEntry } from "../../../data/area_registry";
+import "../../../components/ha-wa-dialog";
+import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import {
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
-} from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts
index 231a652d9d..1354b0a587 100644
--- a/src/panels/config/areas/dialog-floor-registry-detail.ts
+++ b/src/panels/config/areas/dialog-floor-registry-detail.ts
@@ -18,7 +18,7 @@ import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
-import { updateAreaRegistryEntry } from "../../../data/area_registry";
+import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
import type {
FloorRegistryEntry,
FloorRegistryEntryMutableParams,
diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts
index 6264010e15..3b20d744ef 100644
--- a/src/panels/config/areas/ha-config-area-page.ts
+++ b/src/panels/config/areas/ha-config-area-page.ts
@@ -23,11 +23,11 @@ import "../../../components/ha-icon-next";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-tooltip";
-import type { AreaRegistryEntry } from "../../../data/area_registry";
+import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import {
deleteAreaRegistryEntry,
updateAreaRegistryEntry,
-} from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts
index 31a40a765a..502df07cc5 100644
--- a/src/panels/config/areas/ha-config-areas-dashboard.ts
+++ b/src/panels/config/areas/ha-config-areas-dashboard.ts
@@ -31,12 +31,12 @@ import "../../../components/ha-list-item";
import "../../../components/ha-sortable";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
-import type { AreaRegistryEntry } from "../../../data/area_registry";
+import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import {
createAreaRegistryEntry,
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
-} from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import {
createFloorRegistryEntry,
diff --git a/src/panels/config/areas/show-dialog-area-registry-detail.ts b/src/panels/config/areas/show-dialog-area-registry-detail.ts
index ca12d2b87a..f7e79b864a 100644
--- a/src/panels/config/areas/show-dialog-area-registry-detail.ts
+++ b/src/panels/config/areas/show-dialog-area-registry-detail.ts
@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
-} from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
export interface AreaRegistryDetailDialogParams {
entry?: AreaRegistryEntry;
diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts
index 72099734c2..f42d331856 100644
--- a/src/panels/config/automation/add-automation-element-dialog.ts
+++ b/src/panels/config/automation/add-automation-element-dialog.ts
@@ -57,11 +57,11 @@ import {
ACTION_COLLECTIONS,
ACTION_ICONS,
} from "../../../data/action";
-import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
-} from "../../../data/area_registry";
+} from "../../../data/area/area_registry";
+import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
diff --git a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
index 7caa9a148e..e2cf97b4a2 100644
--- a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
+++ b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts
@@ -28,6 +28,10 @@ import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
+import {
+ getAreaDeviceLookup,
+ getAreaEntityLookup,
+} from "../../../../data/area/area_registry";
import {
getAreasNestedInFloors,
type AreaFloorValue,
@@ -35,10 +39,6 @@ import {
type FloorNestedComboBoxItem,
type UnassignedAreasFloorComboBoxItem,
} from "../../../../data/area_floor_picker";
-import {
- getAreaDeviceLookup,
- getAreaEntityLookup,
-} from "../../../../data/area_registry";
import {
getConfigEntries,
type ConfigEntry,
diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts
index ea7d764ad5..3eb99ea442 100644
--- a/src/panels/config/automation/ha-automation-picker.ts
+++ b/src/panels/config/automation/ha-automation-picker.ts
@@ -67,7 +67,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
-import { createAreaRegistryEntry } from "../../../data/area_registry";
+import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import {
deleteAutomation,
diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts
index c45bc75806..0d49a2d2d1 100644
--- a/src/panels/config/dashboard/ha-config-dashboard.ts
+++ b/src/panels/config/dashboard/ha-config-dashboard.ts
@@ -33,10 +33,7 @@ import {
checkForEntityUpdates,
filterUpdateEntitiesParameterized,
} from "../../../data/update";
-import {
- QuickBarMode,
- showQuickBar,
-} from "../../../dialogs/quick-bar/show-dialog-quick-bar";
+import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -375,7 +372,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
};
showQuickBar(this, {
- mode: QuickBarMode.Command,
hint: this.hass.enableShortcuts
? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params)
: undefined,
diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts
index 5d6a9eb5f3..7bac8f3f4a 100644
--- a/src/panels/config/devices/ha-config-devices-dashboard.ts
+++ b/src/panels/config/devices/ha-config-devices-dashboard.ts
@@ -54,7 +54,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
-import { createAreaRegistryEntry } from "../../../data/area_registry";
+import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts
index a30425673d..6bc77d2120 100644
--- a/src/panels/config/scene/ha-scene-dashboard.ts
+++ b/src/panels/config/scene/ha-scene-dashboard.ts
@@ -61,7 +61,7 @@ import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
-import { createAreaRegistryEntry } from "../../../data/area_registry";
+import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
import {
createCategoryRegistryEntry,
diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts
index e01c185347..338ba15c19 100644
--- a/src/panels/config/script/ha-script-picker.ts
+++ b/src/panels/config/script/ha-script-picker.ts
@@ -62,7 +62,7 @@ import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
-import { createAreaRegistryEntry } from "../../../data/area_registry";
+import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
import {
createCategoryRegistryEntry,
diff --git a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts
index 3e15bc3987..846c89cf3b 100644
--- a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts
+++ b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts
@@ -16,7 +16,7 @@ import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-domain-icon";
import "../../../components/ha-svg-icon";
-import type { AreaRegistryEntry } from "../../../data/area_registry";
+import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import { forwardHaptic } from "../../../data/haptics";
import { computeCssVariable } from "../../../resources/css-variables";
import type { HomeAssistant } from "../../../types";
diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts
index b1df8a08db..8313958693 100644
--- a/src/panels/lovelace/hui-root.ts
+++ b/src/panels/lovelace/hui-root.ts
@@ -51,7 +51,7 @@ import "../../components/ha-svg-icon";
import "../../components/ha-tab-group";
import "../../components/ha-tab-group-tab";
import "../../components/ha-tooltip";
-import { createAreaRegistryEntry } from "../../data/area_registry";
+import { createAreaRegistryEntry } from "../../data/area/area_registry";
import type { LovelacePanelConfig } from "../../data/lovelace";
import type {
LovelaceConfig,
@@ -72,10 +72,7 @@ import {
showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
-import {
- QuickBarMode,
- showQuickBar,
-} from "../../dialogs/quick-bar/show-dialog-quick-bar";
+import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle } from "../../resources/styles";
@@ -299,13 +296,11 @@ class HUIRoot extends LitElement {
},
{
icon: mdiMagnify,
- key: "ui.panel.lovelace.menu.search_entities",
+ key: "ui.panel.lovelace.menu.search_home_assistant",
buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar,
visible: !this._editMode && !this.hass.kioskMode,
overflow: this.narrow,
- suffix:
- this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
},
{
icon: mdiCommentProcessingOutline,
@@ -912,7 +907,6 @@ class HUIRoot extends LitElement {
};
showQuickBar(this, {
- mode: QuickBarMode.Entity,
hint: this.hass.enableShortcuts
? this.hass.localize("ui.tips.key_e_tip", params)
: undefined,
diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
index 8a8ffff4b5..4a8e61d0a2 100644
--- a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
+++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts
@@ -13,11 +13,12 @@ import "../../../../../components/ha-entities-display-editor";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-button-prev";
+import type { DisplayItem } from "../../../../../components/ha-items-display-editor";
import "../../../../../components/ha-svg-icon";
import {
updateAreaRegistryEntry,
type AreaRegistryEntry,
-} from "../../../../../data/area_registry";
+} from "../../../../../data/area/area_registry";
import {
haCardSizeLarge,
haCardSizeSmall,
@@ -33,7 +34,6 @@ import {
AREA_STRATEGY_GROUPS,
getAreaGroupedEntities,
} from "../helpers/areas-strategy-helper";
-import type { DisplayItem } from "../../../../../components/ha-items-display-editor";
@customElement("hui-areas-dashboard-strategy-editor")
export class HuiAreasDashboardStrategyEditor
diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts
index e194e09ee1..c1d5714b92 100644
--- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts
+++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts
@@ -4,7 +4,7 @@ import type { EntityFilterFunc } from "../../../../../common/entity/entity_filte
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name";
import { orderCompare } from "../../../../../common/string/compare";
-import type { AreaRegistryEntry } from "../../../../../data/area_registry";
+import type { AreaRegistryEntry } from "../../../../../data/area/area_registry";
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../../../types";
diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
index 6426053a2b..b92b7d4d1f 100644
--- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
+++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts
@@ -7,7 +7,7 @@ import {
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
-import type { AreaRegistryEntry } from "../../../../data/area_registry";
+import type { AreaRegistryEntry } from "../../../../data/area/area_registry";
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type {
diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts
index b42a071126..f12085ff95 100644
--- a/src/state/connection-mixin.ts
+++ b/src/state/connection-mixin.ts
@@ -10,7 +10,7 @@ import {
import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import { promiseTimeout } from "../common/util/promise-timeout";
-import { subscribeAreaRegistry } from "../data/area_registry";
+import { subscribeAreaRegistry } from "../data/area/area_registry";
import { broadcastConnectionStatus } from "../data/connection-status";
import { subscribeDeviceRegistry } from "../data/device/device_registry";
import {
diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts
index f9bb66deeb..f0dd705560 100644
--- a/src/state/quick-bar-mixin.ts
+++ b/src/state/quick-bar-mixin.ts
@@ -1,22 +1,22 @@
import type { PropertyValues } from "lit";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
+import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
import { mainWindow } from "../common/dom/get_main_window";
-import type { QuickBarParams } from "../dialogs/quick-bar/show-dialog-quick-bar";
-import {
- QuickBarMode,
- showQuickBar,
+import { ShortcutManager } from "../common/keyboard/shortcuts";
+import { extractSearchParamsObject } from "../common/url/search-params";
+import type {
+ QuickBarParams,
+ QuickBarSection,
} from "../dialogs/quick-bar/show-dialog-quick-bar";
+import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar";
+import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
+import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
+import type { Redirects } from "../panels/my/ha-panel-my";
import type { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { showToast } from "../util/toast";
import type { HassElement } from "./hass-element";
-import { ShortcutManager } from "../common/keyboard/shortcuts";
-import { extractSearchParamsObject } from "../common/url/search-params";
-import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
-import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
-import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
-import type { Redirects } from "../panels/my/ha-panel-my";
declare global {
interface HASSDomEvents {
@@ -39,13 +39,13 @@ export default >(superClass: T) =>
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
switch (ev.detail.key) {
case "e":
- this._showQuickBar(ev.detail);
+ this._showQuickBar(ev.detail, "entity");
break;
case "c":
- this._showQuickBar(ev.detail, QuickBarMode.Command);
+ this._showQuickBar(ev.detail, "command");
break;
case "d":
- this._showQuickBar(ev.detail, QuickBarMode.Device);
+ this._showQuickBar(ev.detail, "device");
break;
case "m":
this._createMyLink(ev.detail);
@@ -65,19 +65,21 @@ export default >(superClass: T) =>
const shortcutManager = new ShortcutManager();
shortcutManager.add({
// Those are for latin keyboards that have e, c, m keys
- e: { handler: (ev) => this._showQuickBar(ev) },
- c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
+ e: { handler: (ev) => this._showQuickBar(ev, "entity") },
+ c: { handler: (ev) => this._showQuickBar(ev, "command") },
m: { handler: (ev) => this._createMyLink(ev) },
a: { handler: (ev) => this._showVoiceCommandDialog(ev) },
- d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
+ d: { handler: (ev) => this._showQuickBar(ev, "device") },
+ "$mod+k": { handler: (ev) => this._showQuickBar(ev) },
// Workaround see https://github.com/jamiebuilds/tinykeys/issues/130
"Shift+?": { handler: (ev) => this._showShortcutDialog(ev) },
// Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts)
- KeyE: { handler: (ev) => this._showQuickBar(ev) },
- KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
+ KeyE: { handler: (ev) => this._showQuickBar(ev, "entity") },
+ KeyC: { handler: (ev) => this._showQuickBar(ev, "command") },
KeyM: { handler: (ev) => this._createMyLink(ev) },
KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) },
- KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
+ KeyD: { handler: (ev) => this._showQuickBar(ev, "device") },
+ "$mod+KeyK": { handler: (ev) => this._showQuickBar(ev) },
});
}
@@ -102,10 +104,7 @@ export default >(superClass: T) =>
showVoiceCommandDialog(this, this.hass!, { pipeline_id: "last_used" });
}
- private _showQuickBar(
- e: KeyboardEvent,
- mode: QuickBarMode = QuickBarMode.Entity
- ) {
+ private _showQuickBar(e: KeyboardEvent, mode?: QuickBarSection) {
if (!this._canShowQuickBar(e)) {
return;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index 315668041e..562c976d9d 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1338,6 +1338,8 @@
"text": "Home Assistant is running in safe mode, custom integrations and frontend modules are not available. Restart Home Assistant to exit safe mode."
},
"quick-bar": {
+ "commands_title": "Commands",
+ "navigate_title": "Navigate",
"commands": {
"reload": {
"all": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::all%]",
@@ -1423,7 +1425,7 @@
},
"filter_placeholder": "Search entities",
"filter_placeholder_devices": "Search devices",
- "title": "Quick search",
+ "title": "Search Home Assistant",
"key_c_tip": "[%key:ui::tips::key_c_tip%]",
"nothing_found": "Nothing found!"
},
@@ -2148,6 +2150,7 @@
"title": "Searching",
"on_any_page": "On any page",
"on_pages_with_tables": "On pages with tables",
+ "search": "Search Home Assistant",
"search_command": "search command",
"search_entities": "search entities",
"search_devices": "search devices",
@@ -7485,6 +7488,7 @@
"search_entities": "Search entities",
"assist": "Assist",
"assist_tooltip": "Assist",
+ "search_home_assistant": "Search Home Assistant",
"reload_resources": "Reload resources",
"exit_edit_mode": "Done",
"close": "Close",
diff --git a/src/types.ts b/src/types.ts
index 1ffcbb824f..80f523820e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,7 +14,7 @@ import type {
EntityNameOptions,
} from "./common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "./common/translations/localize";
-import type { AreaRegistryEntry } from "./data/area_registry";
+import type { AreaRegistryEntry } from "./data/area/area_registry";
import type { DeviceRegistryEntry } from "./data/device/device_registry";
import type { EntityRegistryDisplayEntry } from "./data/entity/entity_registry";
import type { FloorRegistryEntry } from "./data/floor_registry";
diff --git a/test/common/entity/compute_area_name.test.ts b/test/common/entity/compute_area_name.test.ts
index cd02371ad2..f4b2eff3cf 100644
--- a/test/common/entity/compute_area_name.test.ts
+++ b/test/common/entity/compute_area_name.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { computeAreaName } from "../../../src/common/entity/compute_area_name";
-import type { AreaRegistryEntry } from "../../../src/data/area_registry";
+import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
describe("computeAreaName", () => {
it("returns the trimmed name if present", () => {
diff --git a/test/common/entity/context/context-mock.ts b/test/common/entity/context/context-mock.ts
index e662d22bd5..7180c0483a 100644
--- a/test/common/entity/context/context-mock.ts
+++ b/test/common/entity/context/context-mock.ts
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
-import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
+import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type {
EntityRegistryDisplayEntry,