Compare commits

..
Author SHA1 Message Date
Maarten Lakerveld 1f3b919da0 Fix the bubble tail covering the middle member
The tail is a rotated square whose upper half sits under the bubble. It was
painted on top, so in an open bubble it covered the bottom of the middle
member's frame and selected ring as a white triangle. Both tails now sit
behind their bubble.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld c937c7af6e Address code review: keep the editor alert clear of the zoom control in RTL
The zoom control sits at the physical top-left in either text direction,
but the alert used logical insets, so in RTL its clearance moved to the
right and it covered the control.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld e29156d754 Address code review: recolor the map card from the themes context
The card consumed the theme through hass, whose update gate only lets a
dark mode flip through, so switching between two light or two dark themes
kept the old palette. It now consumes the themes from the UI context, like
ha-map, and rebuilds its entity and trail colors whenever they change.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 4d2a4b2c51 Address code review: follow theme changes and keep bubble focus
The map card rebuilds its entity colors and trail colors when the theme
changes, ha-map redraws editable circles on a theme change, and the
locations editor rebuilds its cached marker elements. A focused cluster
bubble keeps focus on the bubble that replaces it, and a caller naming a
marker by aria-labelledby keeps that name.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld bbc9c03714 Address code review: recolor history trails when the registry changes
The trail colors are memoized with the registry entries as a key, like the
marker colors, so trails recolor with their markers when the registry
arrives or changes.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld dbd4a1714d Address code review: one palette sequence for zones, persons and trackers
Per-domain sequences all started at slot 0, so the first person and the
first zone shared a color and a person could match the zone it was in.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld b570205c1c Address code review: take entity colors from the registry context
The color module kept its own shared registry subscription with listener
counting and connection takeover. The lazy fullEntitiesContext already does
that for the whole app, so the module is now pure: it turns the registry
entries into a palette index, memoized per registry update. Maps ask for
the context only once they draw entities, so an editor's map, as in
onboarding, never requests the registry; the map card and the zone page
consume it directly.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 3405d8078f Address code review: color map card entities from the shared palette
The card kept its own color dictionary, so an entity could have a different
color on every map. It now uses the palette that ha-map and the zone editor
already share, ordered by registry creation; a configured color still wins.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld c16daeb619 Address code review: keep the label mode in cluster bubbles
Bubble avatars only carried the label text and picture, so an entity set to
the icon label mode showed initials once clustered and the attribute mode lost
its unit. The icon flag and unit now travel with the cluster data.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld b5de8071e3 Remove label dedupe
The doubled place labels are a tile issue, not part of the marker redesign,
and the population filter did nothing on the OSMF tiles, which fill in a
default population for every place. It moves to its own branch.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 7234ffaafa Address code review: drop settled zone save queues
The per-zone save chain kept its last resolved promise for the life of the panel.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld e023f2c691 Address code review on the marker redesign, third round
The zone page's updated() override never called its base, so once
willUpdate stopped misusing super.updated the subscribe mixin had no hook
left to run hassSubscribe when hass arrives after connection; the override
calls super.updated now.

Saves for one zone run in order, each looking the entry up when it starts,
so a second response can replace what the first produced, and a failure
only drops the pending values its own request carried. A passive zone that
is not drawn no longer anchors a cluster bubble. A map whose entity list
empties releases the color subscription, not only one whose list is unset.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld c7cdafba29 Release the entity color subscription when a map stops drawing entities
A map whose entities were unset kept its registry subscription, holding
the shared stream open for nothing. It is released along with the
markers, and the three release sites share one helper.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld e20485de7b Count entity color subscriptions from the listener set
The listener set deduplicated a reused callback while a separate counter
counted every call, so the two could disagree and the shared registry
stream could leak or be released early. Each subscription is now its own
listener, the count is the set's size, and unsubscribing twice is a no-op.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld a890cfec6e Redraw markers on a theme change, and test the entity color assignment
Marker and trail colors are resolved from theme variables when drawn, but
a theme change only swapped the base style, so the old palette stayed on
the markers until something else redrew them. Any change to the themes
now redraws entities and trails.

The color assignment gets unit tests: creation order per domain, ties
broken by registry id, the home zone kept out of the palette, muted
passive zones, a stable fallback for entities outside the registry, and
one shared registry stream released with the last subscriber.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 742ad1ba22 Fall back to the palette color for entities without one, recolor zones on theme change
An entity object without a color reached the accuracy circle's paint
with an undefined color; the map's own palette color fills in. The zone
page's markers are memoized on their data, while their colors come from
theme variables, so a theme change now bumps the color version and
recolors them at once.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 407e5fa1b4 Drop the numeric cluster badge styles, follow a replaced connection for colors
Clusters are avatar bubbles now, so the styles for the old count badge
had nothing left to style. The shared entity color subscription is a
module-level singleton keyed to whichever connection subscribed first; a
map arriving with a different connection now takes the stream over
instead of reading colors from the old one.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 03295dac1f Tell the user when a dragged zone edit fails to save
A failed save dropped the pending edit and rethrew, which no caller
awaited, leaving an unhandled rejection and a marker that silently moved
back. The page now shows the error in a dialog instead.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld ddada54553 Address code review on the marker redesign
The selected ring's color came from an inline outline-color that beat the
rule's outline shorthand by precedence, which reads as if the shorthand
won; the inline style now sets a variable the rule consumes, with the
primary color as fallback. A map whose connection context changes
resubscribes to the entity colors instead of staying bound to the old
connection. The zone page's willUpdate called the wrong lifecycle hook on
its base class.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 4f57c943ff Subscribe to entity colors only for maps that draw entities
Every map with a connection subscribed to the entity registry for the
creation-order colors, including editor maps that never draw an entity.
Onboarding's location picker is one, and its mocked server rejects the
registry request, failing the onboarding end-to-end tests. The
subscription now starts when a map first draws entities.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 2b5ab81fa7 Keep dragged values on the zone page until they are saved
The engine used to guess at the host's save state: after a drag it
skipped one update that matched the pre-drag values, taking it for a
re-render with data the save had not yet replaced. That guess is wrong
when a save fails, since the old values are then the truth and the
marker stayed at an unsaved position. The engine now applies every host
update except while a drag is in progress.

The zone page, which knows when it is saving, keeps the dragged values
as pending edits and shows them until the saved data carries them; a
failed save drops them and the marker returns.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld ce18d3745b Drop the editor's drag classes
The engine sets the move cursor on the elements it actually drags, so the
zone marker and the named icon no longer carry a class for it, and the
editor's element cache no longer varies with editability.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 956222c6df Frame person and device markers, ring the selected one
Markers get a card-colored frame with a soft shadow instead of a thin
border in the entity color, and the selected marker gets a primary-color
ring outside that frame. With history trails shown the frame still takes
the entity color, so a marker can be matched to its trail, and the
markers redraw when trails appear or disappear.
2026-09-15 15:59:19 +02:00
Maarten Lakerveld 1d6419310b Redesign map markers with zone circles and avatar cluster bubbles
Zones are drawn as colored circles containing the zone icon, using the
entity's color with the zone radius and GPS accuracy circles matching
it. Marker clusters render as a bubble of up to three rounded square
avatars with a +N chip; when all members are in the same zone and the
bubble would overlap that zone's circle, it floats above the circle,
pinned to the zone's coordinate, with a tail pointing at it. Individual
markers are rounded squares, and while history trails are shown bubble
avatars get borders in their entity color so trails can be matched.
Avatars in bubbles are clickable and open the entity, and the whole
bubble zooms in on its members.

ha-map gains per-entity hide_radius, hide_accuracy, and selected
options for hosts that manage a selection; ha-entity-marker exposes its
background and border width via CSS variables and a selected state with
a thicker border. The OSMF vector tiles carry some places twice (node
and boundary centroid); towns and larger now require a population,
keeping the node, and smaller places get a collision padding.
2026-09-15 15:59:19 +02:00
60 changed files with 1861 additions and 1174 deletions
+1 -1
View File
@@ -106,7 +106,7 @@
"hls.js": "1.7.3",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.15",
"intl-messageformat": "11.2.14",
"js-yaml": "5.4.1",
"leaflet": "1.9.4",
"leaflet.markercluster": "1.5.3",
-6
View File
@@ -1,7 +1 @@
export const stopPropagation = (ev) => ev.stopPropagation();
export const stopKeydownEnterSpacePropagation = (ev: KeyboardEvent) => {
if (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar") {
ev.stopPropagation();
}
};
+8 -35
View File
@@ -123,8 +123,6 @@ interface ManagedMarker {
draggable?: boolean;
onDragEnd?: (location: MapLatLng) => void;
dragging?: boolean;
/** Pre-drag location; the next update echoing it is ignored (see setLocation) */
staleLocation?: MapLatLng;
mlMarker?: MapLibreMarker;
decoration?: MapItemHandle;
removed?: boolean;
@@ -600,19 +598,10 @@ export class MapLibreMapEngine implements MapEngine {
},
clusterData: options.clusterData,
setLocation: (newLocation) => {
// The user's hand wins while dragging; the host is the truth otherwise
if (managed.dragging) {
return;
}
// Skip one update echoing the pre-drag location (the save has not returned yet)
const stale = managed.staleLocation;
managed.staleLocation = undefined;
if (
stale &&
newLocation[0] === stale[0] &&
newLocation[1] === stale[1]
) {
return;
}
managed.location = newLocation;
managed.mlMarker?.setLngLat([newLocation[1], newLocation[0]]);
},
@@ -663,7 +652,6 @@ export class MapLibreMapEngine implements MapEngine {
if (managed.draggable) {
managed.mlMarker.on("dragstart", () => {
managed.dragging = true;
managed.staleLocation = managed.location;
});
managed.mlMarker.on("dragend", () => {
managed.dragging = false;
@@ -801,10 +789,8 @@ export class MapLibreMapEngine implements MapEngine {
resizeHandle?.setAttribute("aria-valuetext", radiusText);
};
// Skip one update echoing the pre-edit values (the save has not returned yet)
// The user's hand wins while dragging; the host is the truth otherwise
let dragging = false;
let staleCenter: MapLatLng | undefined;
let staleRadius: number | undefined;
if (options.resizable) {
const east = pointEastOf(center, options.radius);
@@ -847,8 +833,6 @@ export class MapLibreMapEngine implements MapEngine {
if (keyboardRadius === undefined) {
// Host updates treat a key resize like a drag
dragging = true;
staleCenter = currentCenter;
staleRadius = currentRadius;
}
currentRadius = Math.max(
1,
@@ -875,19 +859,11 @@ export class MapLibreMapEngine implements MapEngine {
[centerMarker, resizeMarker].forEach((handleMarker) => {
handleMarker?.on("dragstart", () => {
dragging = true;
staleCenter = currentCenter;
staleRadius = currentRadius;
});
handleMarker?.on("dragend", () => {
dragging = false;
});
});
const isStale = (candidateCenter: MapLatLng, candidateRadius: number) =>
staleCenter !== undefined &&
staleRadius !== undefined &&
candidateCenter[0] === staleCenter[0] &&
candidateCenter[1] === staleCenter[1] &&
candidateRadius === staleRadius;
if (options.moveable) {
centerMarker.on("drag", () => {
@@ -944,12 +920,6 @@ export class MapLibreMapEngine implements MapEngine {
if (dragging) {
return;
}
const stale = isStale(newCenter, newRadius);
staleCenter = undefined;
staleRadius = undefined;
if (stale) {
return;
}
currentCenter = newCenter;
currentRadius = newRadius;
centerMarker.setLngLat([newCenter[1], newCenter[0]]);
@@ -1219,13 +1189,16 @@ export class MapLibreMapEngine implements MapEngine {
const clusterable = this._markers.filter(
(managed) => managed.options.cluster && !managed.removed
);
// A member that had focus hands it to the icon replacing it; read before
// the open bubble holding it is removed
// A member or bubble that had focus hands it to the icon replacing it;
// read before the bubble holding it is removed
const active = (
this._map.getContainer().getRootNode() as Document | ShadowRoot
).activeElement;
const focusedMember = active
? clusterable.find((managed) => managed.element.contains(active))
? (clusterable.find((managed) => managed.element.contains(active)) ??
this._clusterGroups.find((group) =>
group.iconMarker?.getElement().contains(active)
)?.members[0])
: undefined;
this._clusterGroups.forEach((group) => group.iconMarker?.remove());
+73
View File
@@ -0,0 +1,73 @@
import memoizeOne from "memoize-one";
import { getColorByIndex } from "../color/colors";
import { computeDomain } from "../entity/compute_domain";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
/**
* Map colors for entities, by registry creation order, so an entity has the
* same color on every map. The registry comes from the fullEntitiesContext;
* entities without an entry (e.g. YAML zones) get a color derived from their
* id.
*/
export const HOME_ZONE_ENTITY_ID = "zone.home";
/** Domains whose entities are colored by creation order */
const ORDERED_DOMAINS = ["zone", "person", "device_tracker"];
// One index per registry update, shared by every map on the page. Zones,
// persons and trackers share one sequence, so a person never has the color
// of the zone it is in.
const creationIndex = memoizeOne(
(entries: EntityRegistryEntry[]): Record<string, number> => {
const index: Record<string, number> = {};
entries
.filter(
(entry) =>
ORDERED_DOMAINS.includes(computeDomain(entry.entity_id)) &&
// The home zone has a fixed color and does not take a palette slot
entry.entity_id !== HOME_ZONE_ENTITY_ID
)
.sort((a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id))
.forEach((entry, i) => {
index[entry.entity_id] = i;
});
return index;
}
);
// For entities without a registry entry
const hashIndex = (entityId: string): number => {
let hash = 5381;
for (let i = 0; i < entityId.length; i++) {
hash = (hash * 33 + entityId.charCodeAt(i)) % 2147483647;
}
return hash;
};
/** The palette color of an entity on a map */
export const entityMapColor = (
entityId: string,
entries: EntityRegistryEntry[],
computedStyles: CSSStyleDeclaration
): string =>
getColorByIndex(
creationIndex(entries)[entityId] ?? hashIndex(entityId),
computedStyles
);
/** A zone's color: primary for home, muted for passive, its entity map color otherwise */
export const zoneColor = (
entityId: string,
passive: boolean,
entries: EntityRegistryEntry[],
computedStyles: CSSStyleDeclaration
): string => {
if (entityId === HOME_ZONE_ENTITY_ID) {
return computedStyles.getPropertyValue("--primary-color");
}
if (passive) {
return computedStyles.getPropertyValue("--secondary-text-color");
}
return entityMapColor(entityId, entries, computedStyles);
};
+5 -1
View File
@@ -16,7 +16,11 @@ export const setMarkerAccessibility = (
): void => {
clearMarkerAccessibility(element);
const owned: string[] = [];
if (title && !element.hasAttribute("aria-label")) {
if (
title &&
!element.hasAttribute("aria-label") &&
!element.hasAttribute("aria-labelledby")
) {
element.setAttribute("aria-label", title);
owned.push("aria-label");
}
+67
View File
@@ -0,0 +1,67 @@
import { getContrastedColorHex } from "../color/rgb";
/** The zone marker: a colored circle with the zone's icon or initials, shared by the map and the zone editor */
export const ZONE_CIRCLE_SIZE = 36;
// Content color contrasting the fill; not every theme color parses
export const contrastingZoneContent = (color: string): string => {
try {
return getContrastedColorHex(color.trim());
} catch {
return "#ffffff";
}
};
export const zoneInitials = (name: string): string =>
name
.split(" ")
.map((part) => part[0])
.join("")
.slice(0, 2);
export const createZoneMarkerElement = (options: {
color: string;
icon?: string;
/** Path for an ha-svg-icon, when there is no icon name */
iconPath?: string;
name: string;
}): HTMLElement => {
const element = document.createElement("div");
element.className = "zone-circle";
element.style.backgroundColor = options.color;
element.style.color = contrastingZoneContent(options.color);
if (options.icon) {
const icon = document.createElement("ha-icon");
icon.setAttribute("icon", options.icon);
element.appendChild(icon);
} else if (options.iconPath) {
const icon = document.createElement("ha-svg-icon");
icon.setAttribute("path", options.iconPath);
element.appendChild(icon);
} else {
const initials = document.createElement("span");
initials.textContent = zoneInitials(options.name);
element.appendChild(initials);
}
return element;
};
/** Styles for the zone marker, included by ha-map */
export const zoneMarkerStyles = `
.zone-circle {
width: ${ZONE_CIRCLE_SIZE}px;
height: ${ZONE_CIRCLE_SIZE}px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--card-background-color, #fff);
box-sizing: border-box;
box-shadow: var(--ha-box-shadow-s);
overflow: hidden;
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
--mdc-icon-size: ${ZONE_CIRCLE_SIZE / 2}px;
}
`;
+14 -3
View File
@@ -24,11 +24,13 @@ class HaEntityMarker extends LitElement {
@property({ attribute: "show-icon", type: Boolean }) public showIcon = false;
@property({ type: Boolean, reflect: true }) public selected = false;
protected render() {
return html`
<div
class="marker ${this.entityPicture ? "picture" : ""}"
style=${styleMap({ "border-color": this.entityColor })}
style=${styleMap({ "--ha-marker-selected-color": this.entityColor })}
@click=${this._badgeTap}
>
${
@@ -94,13 +96,22 @@ class HaEntityMarker extends LitElement {
height: var(--ha-marker-size, 48px);
font-size: var(--ha-marker-font-size, var(--ha-font-size-xl));
border-radius: var(--ha-marker-border-radius, 50%);
border: 1px solid var(--ha-marker-color, var(--primary-color));
border: var(--ha-marker-border-width, 3px) solid
var(--ha-marker-color, var(--card-background-color, #fff));
box-shadow: var(--ha-marker-shadow, var(--ha-box-shadow-s));
color: var(--primary-text-color);
background-color: var(--card-background-color);
background-color: var(
--ha-marker-background,
var(--card-background-color)
);
}
.marker.picture {
overflow: hidden;
}
/* A ring in the entity color outside the frame marks the selected marker */
:host([selected]) .marker {
outline: 3px solid var(--ha-marker-selected-color, var(--primary-color));
}
.entity-picture {
background-size: cover;
height: 100%;
+55 -13
View File
@@ -8,14 +8,22 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
import { MAP_MAX_ZOOM } from "../../common/map/base-layer";
import type { MapLatLng } from "../../common/map/map-engine";
import { circleBoundsPoints } from "../../common/map/map-engine";
import { internationalizationContext } from "../../data/context";
import type { HomeAssistantInternationalization, ThemeMode } from "../../types";
import { internationalizationContext, uiContext } from "../../data/context";
import type {
HomeAssistantInternationalization,
HomeAssistantUI,
ThemeMode,
} from "../../types";
import "../ha-alert";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap, HaMapEditableLocation } from "./ha-map";
import type { HaIcon } from "../ha-icon";
import type { HaSvgIcon } from "../ha-svg-icon";
import {
createZoneMarkerElement,
ZONE_CIRCLE_SIZE,
} from "../../common/map/zone-marker";
declare global {
// for fire event
@@ -68,6 +76,11 @@ export class HaLocationsEditor extends LitElement {
@state() private _editingAvailable = true;
// Marker elements bake in theme colors, so they are rebuilt on a theme change
@state()
@consume({ context: uiContext, subscribe: true })
private _ui?: HomeAssistantUI;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: HomeAssistantInternationalization;
@@ -108,7 +121,10 @@ export class HaLocationsEditor extends LitElement {
return html`
<div class="map">
<ha-map
.editableLocations=${this._editableLocations(this.locations)}
.editableLocations=${this._editableLocations(
this.locations,
this._ui?.themes
)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.themeMode=${this.themeMode}
@@ -140,7 +156,14 @@ export class HaLocationsEditor extends LitElement {
}
private _editableLocations = memoizeOne(
(locations?: MarkerLocation[]): HaMapEditableLocation[] => {
(
locations: MarkerLocation[] | undefined,
themes: HomeAssistantUI["themes"] | undefined
): HaMapEditableLocation[] => {
if (themes !== this._elementsThemes) {
this._elementsThemes = themes;
this._elements.clear();
}
const ids = new Set((locations ?? []).map((location) => location.id));
for (const id of this._elements.keys()) {
if (!ids.has(id)) {
@@ -152,7 +175,9 @@ export class HaLocationsEditor extends LitElement {
location: [location.latitude, location.longitude],
radius: location.radius,
element: this._elementFor(location),
elementSize: [ICON_SIZE, ICON_SIZE],
elementSize: location.radius
? [ZONE_CIRCLE_SIZE, ZONE_CIRCLE_SIZE]
: [ICON_SIZE, ICON_SIZE],
title: location.name,
color: location.radius_color,
locationEditable: location.location_editable,
@@ -165,30 +190,46 @@ export class HaLocationsEditor extends LitElement {
// Reused while unchanged, so ha-map moves markers instead of rebuilding them
private _elements = new Map<string, { key: string; element?: HTMLElement }>();
private _elementsThemes?: HomeAssistantUI["themes"];
private _elementFor(location: MarkerLocation): HTMLElement | undefined {
const isZone = !!location.radius;
const key = JSON.stringify([
isZone,
location.icon,
location.iconPath,
location.name,
location.location_editable,
location.radius_color,
]);
const cached = this._elements.get(location.id);
if (cached?.key === key) {
return cached.element;
}
const element = this._createIcon(location);
// The zone marker doubles as the circle's draggable center
const element = isZone
? this._createZoneMarker(location)
: this._createIcon(location);
this._elements.set(location.id, { key, element });
return element;
}
private _createZoneMarker(location: MarkerLocation): HTMLElement {
return createZoneMarkerElement({
color:
location.radius_color ||
getComputedStyle(this).getPropertyValue("--accent-color"),
icon: location.icon,
iconPath: location.iconPath,
name: location.name ?? "",
});
}
private _createIcon(location: MarkerLocation): HTMLElement | undefined {
if (!location.icon && !location.iconPath) {
return undefined;
}
const el = document.createElement("div");
el.className = `named-icon ${
location.location_editable ? "draggable" : ""
}`;
el.className = "named-icon";
if (location.name !== undefined) {
el.innerText = location.name;
}
@@ -296,12 +337,13 @@ export class HaLocationsEditor extends LitElement {
display: block;
height: 100%;
}
/* Over the map, clear of the zoom control, so a fixed-height host shows it */
/* Over the map, clear of the zoom control, so a fixed-height host shows it.
The control sits at the physical top-left in either direction. */
ha-alert {
position: absolute;
top: var(--ha-space-2);
inset-inline-start: 56px;
inset-inline-end: var(--ha-space-2);
left: 56px;
right: var(--ha-space-2);
z-index: 1;
}
`;
+294 -68
View File
@@ -1,4 +1,4 @@
import { consume } from "@lit/context";
import { consume, ContextConsumer } from "@lit/context";
import { isToday } from "date-fns";
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -31,15 +31,23 @@ import type {
} from "../../common/map/map-engine";
import { circleBoundsPoints } from "../../common/map/map-engine";
import { editableCircleStyles } from "../../common/map/editable-circle";
import { entityMapColor, zoneColor } from "../../common/map/entity-map-colors";
import {
createZoneMarkerElement,
ZONE_CIRCLE_SIZE,
zoneMarkerStyles,
} from "../../common/map/zone-marker";
import { filterXSS } from "../../common/util/xss";
import {
configContext,
connectionContext,
formattersContext,
fullEntitiesContext,
internationalizationContext,
statesContext,
uiContext,
} from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { ensureMapTilesToken } from "../../data/map_tiles";
import type {
HomeAssistantConfig,
@@ -211,9 +219,37 @@ export interface HaMapEntity {
unit?: string;
name?: string;
focus?: boolean;
hide_accuracy?: boolean;
hide_radius?: boolean;
selected?: boolean;
}
// Data carried by entity markers for rendering cluster bubbles
interface ClusterData {
entityId: string;
picture?: string;
label: string;
showIcon: boolean;
unit: string;
color?: string;
selected: boolean;
zoneId?: string;
}
const CLUSTER_AVATAR_SIZE = 32;
const CLUSTER_BUBBLE_PADDING = 6;
const CLUSTER_BUBBLE_GAP = 4;
const CLUSTER_MAX_AVATARS = 3;
const CLUSTER_MORE_WIDTH = 28;
const CLUSTER_MORE_MAX = 99;
const CLUSTER_TAIL_SIZE = 10;
// The tail is a rotated square on the bubble's bottom edge, reaching half its diagonal below
const CLUSTER_TAIL_HEIGHT = Math.round((CLUSTER_TAIL_SIZE * Math.SQRT2) / 2);
const CLUSTER_ZONE_SPACING = 2;
const CLUSTER_RADIUS = 40;
// Same-zone markers share the zone's bubble while they span at most this many
// pixels; further apart they show their actual positions
const ZONE_GROUP_RADIUS = 160;
@customElement("ha-map")
export class HaMap extends ReactiveElement {
@@ -264,6 +300,8 @@ export class HaMap extends ReactiveElement {
@property({ attribute: "fit-zones", type: Boolean }) public fitZones = false;
private _zonePositions: Record<string, MapLatLng> = {};
@property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@@ -296,6 +334,11 @@ export class HaMap extends ReactiveElement {
private _resizeObserver?: ResizeObserver;
// Registry creation order decides the palette colors
@state() private _entityReg: EntityRegistryEntry[] = [];
private _registryConsumer?: ContextConsumer<typeof fullEntitiesContext, this>;
private _entityHandles: MapMarkerHandle[] = [];
private _zoneHandles: MapItemHandle[] = [];
@@ -323,6 +366,21 @@ export class HaMap extends ReactiveElement {
this._attachObserver();
}
// Only maps that draw entities ask for the registry; an editor's map, as in
// onboarding, never does. The context provider shares one subscription.
private _watchRegistry(): void {
if (this._registryConsumer || !this.entities?.length) {
return;
}
this._registryConsumer = new ContextConsumer(this, {
context: fullEntitiesContext,
subscribe: true,
callback: (entries) => {
this._entityReg = entries;
},
});
}
private _handleVisibilityChange = async () => {
if (!document.hidden) {
setTimeout(() => {
@@ -385,7 +443,7 @@ export class HaMap extends ReactiveElement {
}
}
if (changedProps.has("clusterMarkers")) {
if (changedProps.has("clusterMarkers") || changedProps.has("_entityReg")) {
this._drawEntities();
}
@@ -401,6 +459,11 @@ export class HaMap extends ReactiveElement {
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
// Cluster bubbles show entity colors only while trails are visible
const oldPaths = changedProps.get("paths") as HaMapPaths[] | undefined;
if (!!oldPaths?.length !== !!this.paths?.length) {
this._engine?.refreshClusters();
}
}
if (changedProps.has("_loaded") || changedProps.has("editableLocations")) {
@@ -433,13 +496,19 @@ export class HaMap extends ReactiveElement {
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
if (
!changedProps.has("themeMode") &&
(!changedProps.has("_ui") ||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
(!changedProps.has("_ui") || (oldUi && oldUi.themes === this._ui.themes))
) {
return;
}
this._updateMapStyle();
// Marker, trail and circle colors were resolved from the theme when drawn
this._drawEntities();
this._drawPaths();
if (this._editableHandles.size) {
this._removeEditableLocations();
this._drawEditableLocations();
}
}
private get _darkMode() {
@@ -1031,16 +1100,36 @@ export class HaMap extends ReactiveElement {
engine.setClustering(null);
return;
}
this._watchRegistry();
const computedStyles = getComputedStyle(this);
const zoneColor = computedStyles.getPropertyValue("--accent-color");
const passiveZoneColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
const darkPrimaryColor = computedStyles.getPropertyValue(
"--dark-primary-color"
);
// A person's state is "home" for the home zone, the zone name otherwise
const zoneByState: Record<string, string> = {};
this._zonePositions = {};
for (const entity of this.entities) {
const stateObj = states[getEntityId(entity)];
// A zone that is not drawn cannot anchor a bubble either
if (
stateObj &&
computeStateDomain(stateObj) === "zone" &&
(this.renderPassive || !stateObj.attributes.passive)
) {
zoneByState[
stateObj.entity_id === "zone.home"
? "home"
: computeStateName(stateObj)
] = stateObj.entity_id;
if (
typeof stateObj.attributes.latitude === "number" &&
typeof stateObj.attributes.longitude === "number"
) {
this._zonePositions[stateObj.entity_id] = [
stateObj.attributes.latitude,
stateObj.attributes.longitude,
];
}
}
}
for (const entity of this.entities) {
const stateObj = states[getEntityId(entity)];
@@ -1069,26 +1158,29 @@ export class HaMap extends ReactiveElement {
continue;
}
const zoneMarkerColor = passive ? passiveZoneColor : zoneColor;
const hideRadius = typeof entity !== "string" && entity.hide_radius;
// A host-set color wins, except passive zones are always muted
const markerColor =
!passive && typeof entity !== "string" && entity.color
? entity.color
: zoneColor(
stateObj.entity_id,
!!passive,
this._entityReg,
computedStyles
);
if (radius) {
if (!hideRadius && radius) {
this._zoneHandles.push(
engine.addCircle(position, { radius, color: zoneMarkerColor })
engine.addCircle(position, { radius, color: markerColor })
);
}
// create icon
const iconEl = document.createElement("div");
iconEl.className = `zone-icon ${this._darkMode ? "dark" : "light"}`;
if (icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconEl.appendChild(el);
} else {
const el = document.createElement("span");
el.textContent = title;
iconEl.appendChild(el);
}
const circleEl = createZoneMarkerElement({
color: markerColor,
icon,
name: title,
});
if (this.interactiveZones) {
const openMoreInfo = (ev: Event) => {
@@ -1097,8 +1189,8 @@ export class HaMap extends ReactiveElement {
entityId: stateObj.entity_id,
});
};
iconEl.addEventListener("click", openMoreInfo);
iconEl.addEventListener("keydown", (ev) => {
circleEl.addEventListener("click", openMoreInfo);
circleEl.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
openMoreInfo(ev);
@@ -1106,10 +1198,9 @@ export class HaMap extends ReactiveElement {
});
}
const zoneIconSize = this._getMarkerSize(computedStyles) / 2;
this._zoneHandles.push(
engine.addMarker(iconEl, position, {
size: [zoneIconSize, zoneIconSize],
engine.addMarker(circleEl, position, {
size: [ZONE_CIRCLE_SIZE, ZONE_CIRCLE_SIZE],
interactive: this.interactiveZones,
title,
})
@@ -1119,7 +1210,7 @@ export class HaMap extends ReactiveElement {
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
) {
if (radius) {
if (!hideRadius && radius) {
this._focusZonePoints.push(...circleBoundsPoints(position, radius));
} else {
this._focusZonePoints.push(position);
@@ -1163,19 +1254,42 @@ export class HaMap extends ReactiveElement {
entityPicture && (typeof entity === "string" || !entity.label_mode)
? this._connection.hassUrl(entityPicture)
: "";
// A host may leave the color to the map
const entityColor =
(typeof entity !== "string" ? entity.color : undefined) ||
entityMapColor(getEntityId(entity), this._entityReg, computedStyles);
entityMarker.entityColor = entityColor;
if (typeof entity !== "string") {
entityMarker.entityColor = entity.color;
entityMarker.selected = entity.selected ?? false;
}
const clusterData: ClusterData = {
entityId: getEntityId(entity),
picture: entityMarker.entityPicture || undefined,
label: entityName,
showIcon: entityMarker.showIcon,
unit: entityMarker.entityUnit ?? "",
color: entityColor,
selected: typeof entity !== "string" && (entity.selected ?? false),
zoneId: ["person", "device_tracker"].includes(
computeStateDomain(stateObj)
)
? zoneByState[stateObj.state]
: undefined,
};
const showAccuracy =
!!gpsAccuracy && !(typeof entity !== "string" && entity.hide_accuracy);
const markerSize = this._getMarkerSize(computedStyles);
this._entityHandles.push(
engine.addMarker(entityMarker, position, {
size: [markerSize, markerSize],
title,
cluster: true,
// create circle around if entity has accuracy
decoration: gpsAccuracy
? { radius: gpsAccuracy, color: darkPrimaryColor }
clusterData,
decoration: showAccuracy
? { radius: gpsAccuracy!, color: entityColor }
: undefined,
})
);
@@ -1187,22 +1301,97 @@ export class HaMap extends ReactiveElement {
engine.setClustering(
this.clusterMarkers
? { radius: CLUSTER_RADIUS, iconBuilder: this._createClusterIcon }
? {
radius: CLUSTER_RADIUS,
iconBuilder: this._createClusterBubble,
// Everyone in a zone shares its bubble until zooming spreads them
groupKey: (marker) => (marker.clusterData as ClusterData)?.zoneId,
groupRadius: ZONE_GROUP_RADIUS,
}
: null
);
}
// Renders a marker cluster as a circle with the member count
private _createClusterIcon = (members: MapMarkerHandle[]): MapClusterIcon => {
const size = Math.round(
this._getMarkerSize(getComputedStyle(this)) * (2 / 3)
);
const element = document.createElement("div");
element.className = "marker-cluster";
const count = document.createElement("span");
count.textContent = String(members.length);
element.appendChild(count);
return { element, size: [size, size] };
// Renders a marker cluster as a bubble of its members' avatars
private _createClusterBubble = (
members: MapMarkerHandle[]
): MapClusterIcon => {
const data = members.map((member) => member.clusterData as ClusterData);
const shown = data.slice(0, CLUSTER_MAX_AVATARS);
const hidden = data.length - shown.length;
// With history trails shown, colored borders match avatars to trails
const showColors = !!this.paths?.length;
const bubble = document.createElement("div");
bubble.className = "cluster-bubble";
for (const member of shown) {
const avatar = document.createElement("ha-entity-marker");
avatar.entityId = member?.entityId;
avatar.entityName = member?.label ?? "";
avatar.entityUnit = member?.unit ?? "";
avatar.showIcon = member?.showIcon ?? false;
avatar.entityPicture = member?.picture ?? "";
avatar.entityColor = member?.color;
if (showColors) {
avatar.style.setProperty(
"--ha-marker-color",
member?.color ?? "var(--primary-color)"
);
avatar.style.setProperty("--ha-marker-border-width", "2px");
}
if (member?.selected) {
avatar.selected = true;
}
bubble.appendChild(avatar);
}
let width =
shown.length * CLUSTER_AVATAR_SIZE +
(shown.length - 1) * CLUSTER_BUBBLE_GAP +
2 * CLUSTER_BUBBLE_PADDING;
if (hidden > 0) {
const more = document.createElement("span");
more.className = "more";
more.textContent =
hidden > CLUSTER_MORE_MAX ? `${CLUSTER_MORE_MAX}+` : `+${hidden}`;
bubble.appendChild(more);
width += CLUSTER_MORE_WIDTH + CLUSTER_BUBBLE_GAP;
}
// A cluster of one zone's occupants attaches to that zone's marker
const zoneId = data[0]?.zoneId;
const zonePosition = zoneId ? this._zonePositions[zoneId] : undefined;
const atZone =
!!zoneId &&
!!zonePosition &&
data.every((member) => member?.zoneId === zoneId);
let height = CLUSTER_AVATAR_SIZE + 2 * CLUSTER_BUBBLE_PADDING;
let root: HTMLElement = bubble;
if (atZone) {
root = document.createElement("div");
root.className = "cluster-marker";
const tail = document.createElement("div");
tail.className = "cluster-bubble-tail";
root.append(bubble, tail);
height += CLUSTER_TAIL_HEIGHT;
}
return {
element: root,
size: [width, height],
// Float above the zone circle, tail pointing at it
...(atZone && zonePosition
? {
location: zonePosition,
anchor: [
width / 2,
height + ZONE_CIRCLE_SIZE / 2 + CLUSTER_ZONE_SPACING,
] as [number, number],
}
: {}),
};
};
private _drawScaleRuler(): void {
@@ -1263,6 +1452,7 @@ export class HaMap extends ReactiveElement {
display: flex;
flex-direction: column;
align-items: center;
isolation: isolate;
}
.cluster-open-members {
display: flex;
@@ -1276,6 +1466,13 @@ export class HaMap extends ReactiveElement {
border-radius: 14px;
box-shadow: var(--ha-box-shadow-s);
}
/* Both tails are a rotated square whose upper half sits under the bubble;
drawn behind it, so it never covers a member's frame or selected ring */
.cluster-open-tail,
.cluster-bubble-tail {
position: relative;
z-index: -1;
}
.cluster-open-tail {
width: 10px;
height: 10px;
@@ -1349,17 +1546,59 @@ export class HaMap extends ReactiveElement {
.leaflet-pane {
z-index: 0 !important;
}
/* Zone icons, sized like the Leaflet divIcon they replaced */
.zone-icon {
.cluster-marker {
display: flex;
flex-direction: column;
align-items: center;
isolation: isolate;
}
.cluster-bubble-tail {
width: ${CLUSTER_TAIL_SIZE}px;
height: ${CLUSTER_TAIL_SIZE}px;
margin-top: ${-CLUSTER_TAIL_SIZE / 2}px;
border-radius: 2px;
background: var(--card-background-color, #fff);
transform: rotate(45deg);
}
.cluster-bubble {
display: flex;
align-items: center;
gap: ${CLUSTER_BUBBLE_GAP}px;
padding: ${CLUSTER_BUBBLE_PADDING}px;
box-sizing: border-box;
background: var(--card-background-color, #fff);
border-radius: 14px;
box-shadow: var(--ha-box-shadow-s);
--ha-marker-size: ${CLUSTER_AVATAR_SIZE}px;
--ha-marker-color: transparent;
--ha-marker-border-width: 1px;
--ha-marker-shadow: none;
--ha-marker-font-size: var(--ha-font-size-s);
/* distinguish letter tiles from the bubble background */
--ha-marker-background: var(--ha-color-fill-neutral-quiet-resting);
}
.cluster-bubble .more {
flex: none;
width: ${CLUSTER_MORE_WIDTH}px;
height: ${CLUSTER_AVATAR_SIZE}px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--primary-text-color);
border-radius: 10px;
background: var(--ha-color-fill-neutral-quiet-resting, #f0f0f0);
color: var(--primary-text-color, #212121);
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
}
.zone-icon.dark {
color: #ffffff;
/* Markers are rounded squares to match the cluster bubble avatars */
ha-entity-marker {
--ha-marker-border-radius: var(--ha-border-radius-lg);
}
.cluster-bubble ha-entity-marker {
flex: none;
--ha-marker-border-radius: 10px;
}
${unsafeCSS(zoneMarkerStyles)}
.leaflet-control,
.leaflet-top,
.leaflet-bottom {
@@ -1410,19 +1649,6 @@ export class HaMap extends ReactiveElement {
ha-icon {
--mdc-icon-size: calc(var(--ha-marker-size, 48px) / 2);
}
.marker-cluster {
box-sizing: border-box;
background-clip: padding-box;
background-color: var(--primary-color);
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary-color);
font-size: var(--ha-font-size-m);
}
`;
}
+1 -19
View File
@@ -1,6 +1,4 @@
import { ensureArray } from "../common/array/ensure-array";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { Context, HomeAssistant, ServiceCallRequest } from "../types";
import type { Context, HomeAssistant } from "../types";
import type { Action } from "./script";
export const callExecuteScript = (
@@ -24,19 +22,3 @@ export const serviceCallWillDisconnect = (
"update.home_assistant_core_update",
"update.home_assistant_operating_system_update",
].includes(serviceData?.entity_id));
// Core merges the target into the service data, so a target entity_id
// replaces the legacy service data one rather than adding to it. Its schema
// also accepts comma separated ids and lowercases them.
export const getServiceCallEntityIds = (
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"]
): string[] => [
...new Set(
(ensureArray(target?.entity_id ?? serviceData?.entity_id) ?? [])
.filter((id): id is string => typeof id === "string")
.flatMap((id) => id.split(","))
.map((id) => id.trim().toLowerCase())
.filter(isValidEntityId)
),
];
-10
View File
@@ -184,15 +184,6 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
};
}
interface EMOutgoingMessageEntityControlled extends EMMessage {
type: "entity/controlled";
payload: {
entity_ids: string[];
domain: string;
service: string;
};
}
interface EMOutgoingMessageMoreInfoOpened extends EMMessage {
type: "more_info/opened";
payload: {
@@ -248,7 +239,6 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo
| EMOutgoingMessageEntityControlled
| EMOutgoingMessageFocusElement
| EMOutgoingMessageReloadAndClearCache
| EMOutgoingMessageAssistSettings;
@@ -9,9 +9,9 @@ import {
mdiVideo,
mdiWebhook,
} from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { TemplateResult } from "lit";
import { formatDate } from "../../../../common/datetime/format_date";
import { relativeTime } from "../../../../common/datetime/relative_time";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -20,13 +20,11 @@ import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/list/ha-list-base";
import "../../../../components/list/ha-list-nav";
import type { BackupConfig } from "../../../../data/backup";
import { cloudBackupEnabled, cloudBackupHealth } from "../../../../data/backup";
import { cloudBackupHealth, cloudBackupEnabled } from "../../../../data/backup";
import type {
CloudStatusLoggedIn,
SubscriptionInfo,
@@ -262,8 +260,8 @@ export class CloudAccountOverview extends LitElement {
/>
</div>
<div class="card-content">
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.email"
@@ -285,16 +283,16 @@ export class CloudAccountOverview extends LitElement {
@click=${this._toggleEmail}
></ha-icon-button>
</span>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.subscription"
)}</span
>
<span slot="supporting-text">${this._subscriptionDetail()}</span>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</div>
<div class="card-actions split">
<ha-button
@@ -350,11 +348,7 @@ export class CloudAccountOverview extends LitElement {
"ui.panel.config.cloud.account.overview.features_intro"
)}
</p>
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.cloud.account.overview.features_title"
)}
>
<ha-md-list>
${this._featureRow(
mdiEarth,
this.hass.localize(
@@ -417,7 +411,7 @@ export class CloudAccountOverview extends LitElement {
webhookStatus,
"/config/cloud/webhooks"
)}
</ha-list-nav>
</ha-md-list>
</div>
</ha-card>
`;
@@ -430,12 +424,12 @@ export class CloudAccountOverview extends LitElement {
href: string
): TemplateResult {
return html`
<ha-list-item-button href=${href}>
<ha-md-list-item type="link" href=${href}>
<ha-svg-icon slot="start" .path=${icon}></ha-svg-icon>
<span slot="headline">${title}</span>
<span slot="supporting-text">${supporting}</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
@@ -667,6 +661,9 @@ export class CloudAccountOverview extends LitElement {
width: 36px;
height: auto;
}
ha-md-list-item {
--md-item-overflow: visible;
}
.muted {
color: var(--secondary-text-color);
}
@@ -695,8 +692,10 @@ export class CloudAccountOverview extends LitElement {
.status-dot.disabled {
background-color: var(--error-color);
}
ha-list-base {
--ha-row-item-padding-inline: 0;
ha-md-list {
padding: 0;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
.extras-header {
display: flex;
@@ -722,12 +721,14 @@ export class CloudAccountOverview extends LitElement {
padding-inline: var(--ha-space-4);
margin-top: 0;
}
.extras-content ha-list-nav {
.extras-content ha-md-list {
padding-top: 0;
--ha-row-item-padding-inline: var(--ha-space-4);
--md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: var(--ha-space-4);
}
.extras-content ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-2);
.extras-content ha-md-list-item {
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
}
.extras-content ha-svg-icon[slot="start"],
.extras-content ha-icon-next {
@@ -11,9 +11,9 @@ import { relativeTime } from "../../../../common/datetime/relative_time";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-spinner";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/list/ha-list-base";
import type { BackupConfig, BackupInfo } from "../../../../data/backup";
import {
CLOUD_AGENT,
@@ -137,8 +137,8 @@ export class CloudBackupPref extends LitElement {
)}
>
<div class="card-content">
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">
${
@@ -159,11 +159,11 @@ export class CloudBackupPref extends LitElement {
)
}
</span>
</ha-list-item-base>
</ha-md-list-item>
${
cloudAgent
? html`
<ha-list-item-base>
<ha-md-list-item>
<ha-svg-icon
slot="start"
.path=${mdiHarddisk}
@@ -171,8 +171,8 @@ export class CloudBackupPref extends LitElement {
<span slot="headline">
${bytesToString(cloudAgent.size)}
</span>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon
slot="start"
.path=${mdiShieldLock}
@@ -188,11 +188,11 @@ export class CloudBackupPref extends LitElement {
)
}
</span>
</ha-list-item-base>
</ha-md-list-item>
`
: nothing
}
<ha-list-item-base>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">
${
@@ -213,8 +213,8 @@ export class CloudBackupPref extends LitElement {
)
}
</span>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</div>
<div class="card-actions">
<ha-button appearance="filled" href="/config/backup?historyBack=1">
@@ -308,12 +308,15 @@ export class CloudBackupPref extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-list-base {
--ha-row-item-padding-inline: 0;
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-list-item-base {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 40px;
ha-md-list-item {
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-one-line-container-height: 40px;
}
ha-svg-icon[slot="start"] {
color: var(--secondary-text-color);
@@ -4,10 +4,10 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import "../../../../components/ha-tip";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/list/ha-list-base";
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaSwitch } from "../../../../components/ha-switch";
@@ -179,8 +179,8 @@ export class CloudRemotePref extends LitElement {
)}
>
<div class="card-content">
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.external_activation"
@@ -196,8 +196,8 @@ export class CloudRemotePref extends LitElement {
.checked=${remote_allow_remote_enable}
@change=${this._toggleAllowRemoteEnabledChanged}
></ha-switch>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
@@ -232,8 +232,8 @@ export class CloudRemotePref extends LitElement {
"ui.panel.config.cloud.account.remote.more_info"
)}
</ha-button>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
<ha-tip .hass=${this.hass}>
@@ -318,8 +318,13 @@ export class CloudRemotePref extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-list-base {
--ha-row-item-padding-inline: 0;
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-tip {
max-width: 600px;
@@ -5,26 +5,26 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-language-picker";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-textarea";
import "../../../../components/ha-tip";
import "../../../../components/voice-assistant-brand-icon";
import type {
HaSelectOption,
HaSelectSelectEvent,
} from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-textarea";
import type { HaTextArea } from "../../../../components/ha-textarea";
import "../../../../components/ha-tip";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/list/ha-list-base";
import "../../../../components/voice-assistant-brand-icon";
import { showAutomationEditor } from "../../../../data/automation";
import type { CloudStatusLoggedIn } from "../../../../data/cloud";
import { updateCloudPref } from "../../../../data/cloud";
@@ -153,8 +153,8 @@ export class CloudTTSPref extends LitElement {
)}
>
<div class="card-content">
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.cloud.account.tts.default_language"
@@ -176,8 +176,8 @@ export class CloudTTSPref extends LitElement {
@value-changed=${this._handleLanguageChange}
>
</ha-language-picker>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.cloud.account.tts.default_voice"
@@ -199,8 +199,8 @@ export class CloudTTSPref extends LitElement {
}))}
>
</ha-select>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.components.media-browser.tts.selected_voice_id"
@@ -215,8 +215,8 @@ export class CloudTTSPref extends LitElement {
)}
@click=${this._copyVoiceId}
></ha-icon-button>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
<div class="try-tts">
<span class="try-heading">
${this.hass.localize(
@@ -541,8 +541,13 @@ export class CloudTTSPref extends LitElement {
gap: var(--ha-space-2);
flex-wrap: wrap;
}
ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-4);
ha-md-list {
background: none;
--md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: var(--ha-space-4);
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-language-picker,
ha-select {
@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import { mdiContentCopy, mdiRestore } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -7,6 +6,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { consume } from "@lit/context";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
@@ -19,24 +19,23 @@ import type {
LocalizeKeys,
} from "../../../common/translations/localize";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-color-picker";
import "../../../components/ha-dropdown-item";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker";
import "../../../components/ha-labels-picker";
import "../../../components/ha-list-item";
import "../../../components/ha-md-list-item";
import "../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-state-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/input/ha-input";
import "../../../components/item/ha-list-item-button";
import "../../../components/item/ha-row-item";
import "../../../components/list/ha-list-base";
import {
CAMERA_ORIENTATIONS,
CameraEntityFeature,
@@ -46,16 +45,16 @@ import {
STREAM_TYPE_HLS,
updateCameraPrefs,
} from "../../../data/camera";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../../../data/context/dirty-state";
import type { ConfigEntry } from "../../../data/config_entries";
import { deleteConfigEntry } from "../../../data/config_entries";
import {
createConfigFlow,
handleConfigFlowStep,
} from "../../../data/config_flow";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../../../data/context/dirty-state";
import type { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import { updateDeviceRegistryEntry } from "../../../data/device/device_registry";
@@ -628,7 +627,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
${
SWITCH_AS_DOMAINS_INVERT.includes(this._switchAsDomain)
? html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.invert.label"
@@ -644,7 +643,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
.checked=${!!this.entry.options?.switch_as_x?.invert}
@change=${this._switchAsInvertChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`
: nothing
} `
@@ -961,7 +960,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
${
this._cameraPrefs
? html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.preload_stream"
@@ -978,8 +977,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@change=${this._handleCameraPrefsChanged}
></ha-switch>
</ha-row-item>
<ha-row-item>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
@@ -1007,97 +1006,103 @@ export class EntityRegistrySettingsEditor extends LitElement {
.value=${this._cameraPrefs.orientation.toString()}
>
</ha-select>
</ha-row-item>
</ha-md-list-item>
`
: nothing
}
<ha-list-base>
${
this.helperConfigEntry &&
this.helperConfigEntry.supports_options &&
this.helperConfigEntry.domain !== "switch_as_x"
? html`
<ha-list-item-button
class="menu-item"
.disabled=${this.disabled}
@click=${this._showOptionsFlow}
${
this.helperConfigEntry &&
this.helperConfigEntry.supports_options &&
this.helperConfigEntry.domain !== "switch_as_x"
? html`
<ha-list-item
class="menu-item"
twoline
hasMeta
.disabled=${this.disabled}
@click=${this._showOptionsFlow}
>
<span
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state_secondary",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`
: nothing
}
<ha-list-item-button
class="menu-item"
.disabled=${this.disabled}
@click=${this._handleVoiceAssistantsClicked}
<span slot="secondary"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state_secondary",
{
integration: domainToName(
this.hass.localize,
this.helperConfigEntry.domain
),
}
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing
}
<ha-list-item
class="menu-item"
twoline
hasMeta
.disabled=${this.disabled}
@click=${this._handleVoiceAssistantsClicked}
>
<span
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants"
)}</span
>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants"
)}</span
>
<span slot="supporting-text">
${
this.entry.aliases.filter((a) => a !== null).length
? this.entry.aliases
.filter((a): a is string => a !== null)
.join(", ")
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)
}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
${
domain === "vacuum" &&
stateObj &&
supportsFeature(stateObj, VacuumEntityFeature.CLEAN_AREA)
? html`
<ha-list-item-button
class="menu-item"
.disabled=${this.disabled}
@click=${this._handleVacuumSegmentMappingClicked}
<span slot="secondary">
${
this.entry.aliases.filter((a) => a !== null).length
? this.entry.aliases
.filter((a): a is string => a !== null)
.join(", ")
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)
}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
${
domain === "vacuum" &&
stateObj &&
supportsFeature(stateObj, VacuumEntityFeature.CLEAN_AREA)
? html`
<ha-list-item
class="menu-item"
twoline
hasMeta
.disabled=${this.disabled}
@click=${this._handleVacuumSegmentMappingClicked}
>
<span
>${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.title"
)}</span
>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.title"
)}</span
>
<span slot="supporting-text">
${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.description"
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`
: nothing
}
</ha-list-base>
<span slot="secondary">
${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.description"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing
}
${
this._disabledBy &&
this._disabledBy !== "user" &&
@@ -1115,7 +1120,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
: nothing
}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
@@ -1138,7 +1143,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
@change=${this._enabledChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
this._hiddenBy && this._hiddenBy !== "user"
@@ -1155,7 +1160,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
: nothing
}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.visible_label"
@@ -1172,12 +1177,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled || this._disabledBy}
@change=${this._hiddenChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
this.entry.device_id
? html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.use_device_area"
@@ -1213,7 +1218,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@change=${this._useDeviceAreaChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
this._areaId || this._noDeviceArea
? html`<ha-area-picker
@@ -1806,10 +1811,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
margin-inline-start: 0;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-row-item ha-select {
ha-md-list-item ha-select {
width: auto;
}
ha-input,
@@ -1821,11 +1823,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
width: 100%;
}
.menu-item {
--ha-row-item-padding-inline: 0;
border-radius: var(--ha-border-radius-sm);
margin-top: 3px;
margin-bottom: 3px;
overflow: hidden;
--mdc-list-side-padding: 13px;
}
.entityId {
direction: ltr;
@@ -9,16 +9,15 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { round } from "../../../common/number/round";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import { sanitizeHttpUrl } from "../../../common/url/sanitize-http-url";
import "../../../components/animation/ha-fade-in";
import "../../../components/chart/ha-chart-base";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-nav";
import type { ConfigEntry } from "../../../data/config_entries";
import { subscribeConfigEntries } from "../../../data/config_entries";
import type {
@@ -306,29 +305,24 @@ class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
${
documentationURL
? html`
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.hardware.documentation"
)}
<ha-md-list-item
.href=${documentationURL}
type="link"
target="_blank"
rel="noopener noreferrer"
>
<ha-list-item-button
.href=${documentationURL}
target="_blank"
rel="noopener noreferrer"
<span
>${this.hass.localize(
"ui.panel.config.hardware.documentation"
)}</span
>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.hardware.documentation"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.hardware.documentation_description"
)}</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.hardware.documentation_description"
)}</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`
: nothing
}
@@ -140,13 +140,8 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._domain = params.domain;
this._item = undefined;
if (this._domain && this._domain in HELPERS) {
this._loading = true;
try {
await HELPERS[this._domain].import();
this._initDirtyTracking({ type: "deep" }, undefined);
} finally {
this._loading = false;
}
await HELPERS[this._domain].import();
this._initDirtyTracking({ type: "deep" }, undefined);
}
this._open = true;
await this.updateComplete;
@@ -190,7 +185,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
let content: TemplateResult;
let footer: TemplateResult | typeof nothing = nothing;
if (this._domain && !this._loading) {
if (this._domain) {
content = html`
<div class="form" @value-changed=${this._valueChanged}>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
@@ -1,9 +1,9 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-dialog";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import { ERROR_STATES, RECOVERABLE_STATES } from "../../../data/config_entries";
import type { HomeAssistant } from "../../../types";
import type { PickConfigEntryDialogParams } from "./show-pick-config-entry-dialog";
@@ -43,22 +43,21 @@ export class DialogPickConfigEntry extends LitElement {
header-title=${title}
@closed=${this._dialogClosed}
>
<ha-list-base>
<ha-md-list>
${this._params.configEntries.map(
(entry) =>
html`<ha-list-item-button
html`<ha-md-list-item
type="button"
@click=${this._itemPicked}
.entry=${entry}
.disabled=${
!ERROR_STATES.includes(entry.state) &&
!RECOVERABLE_STATES.includes(entry.state)
}
><span slot="headline"
>${entry.title}</span
></ha-list-item-button
>${entry.title}</ha-md-list-item
>`
)}
</ha-list-base>
</ha-md-list>
</ha-dialog>
`;
}
@@ -4,17 +4,15 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import { stopKeydownEnterSpacePropagation } from "../../../../../common/dom/stop_propagation";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import type {
BluetoothAllocationsData,
BluetoothScannerState,
@@ -193,7 +191,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
>
<div class="container">
<ha-card>
<ha-list-base>
<ha-md-list>
${this._renderAdaptersList(enabledDevices)}
${disabledDevices.map(({ device, entry }) => {
const areaName =
@@ -207,7 +205,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
.filter(Boolean)
.join(" · ");
return html`
<ha-list-item-base class="disabled">
<ha-md-list-item class="disabled">
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
<div slot="headline">
${computeDeviceName(device) || entry.title}
@@ -220,12 +218,12 @@ export class BluetoothAdapterInfoPage extends LitElement {
>
${this.hass.localize("ui.common.enable")}
</ha-button>
</ha-list-item-base>
</ha-md-list-item>
`;
})}
${disabledEntriesWithoutDevice.map(
(entry) => html`
<ha-list-item-base class="disabled">
<ha-md-list-item class="disabled">
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
<div slot="headline">${entry.title}</div>
<div slot="supporting-text">
@@ -240,10 +238,10 @@ export class BluetoothAdapterInfoPage extends LitElement {
>
${this.hass.localize("ui.common.enable")}
</ha-button>
</ha-list-item-base>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
<div class="card-actions">
<ha-button
appearance="plain"
@@ -264,13 +262,13 @@ export class BluetoothAdapterInfoPage extends LitElement {
devices: { device: DeviceRegistryEntry; entry: ConfigEntry }[]
) {
if (devices.length === 0) {
return html`<ha-list-item-base>
return html`<ha-md-list-item>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</div>
</ha-list-item-base>`;
</ha-md-list-item>`;
}
return devices.map(({ device, entry }) => {
@@ -316,7 +314,8 @@ export class BluetoothAdapterInfoPage extends LitElement {
);
return html`
<ha-list-item-button
<ha-md-list-item
type="link"
href=${`/config/bluetooth/connection-monitor?source=${btAddress}`}
>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
@@ -329,7 +328,6 @@ export class BluetoothAdapterInfoPage extends LitElement {
.path=${mdiCogOutline}
.entry=${entry}
@click=${this._openOptionFlow}
@keydown=${stopKeydownEnterSpacePropagation}
.label=${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}
@@ -337,7 +335,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
: nothing
}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
${
hasMismatch && scannerDetails
? this._renderScannerMismatchWarning(
@@ -501,14 +499,22 @@ export class BluetoothAdapterInfoPage extends LitElement {
margin: 0 auto var(--ha-space-4);
}
ha-list-item-button ha-svg-icon[slot="start"],
ha-list-item-base ha-svg-icon[slot="start"] {
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-md-list-item ha-svg-icon[slot="start"] {
color: var(--secondary-text-color);
}
ha-list-item-base.disabled ha-svg-icon[slot="start"],
ha-list-item-base.disabled [slot="headline"],
ha-list-item-base.disabled [slot="supporting-text"] {
ha-md-list-item.disabled ha-svg-icon[slot="start"],
ha-md-list-item.disabled [slot="headline"],
ha-md-list-item.disabled [slot="supporting-text"] {
opacity: 0.5;
}
@@ -7,16 +7,16 @@ import {
mdiLinkVariant,
mdiVectorPolyline,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type {
BluetoothAllocationsData,
BluetoothDeviceData,
@@ -213,12 +213,11 @@ export class BluetoothConfigDashboard extends LitElement {
</ha-button>
</div>
<div class="card-content network-card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.bluetooth.my_network"
)}
>
<ha-list-item-button href="/config/bluetooth/adapter-info">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/bluetooth/adapter-info"
>
<ha-svg-icon
slot="start"
.path=${mdiAccessPoint}
@@ -230,9 +229,10 @@ export class BluetoothConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
<ha-list-item-button
<ha-md-list-item
type="link"
href="/config/bluetooth/connection-monitor"
>
<ha-svg-icon
@@ -246,9 +246,10 @@ export class BluetoothConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
<ha-list-item-button
<ha-md-list-item
type="link"
href="/config/bluetooth/advertisement-monitor"
>
<ha-svg-icon slot="start" .path=${mdiBroadcast}></ha-svg-icon>
@@ -259,8 +260,8 @@ export class BluetoothConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
</div>
@@ -285,6 +286,11 @@ export class BluetoothConfigDashboard extends LitElement {
margin-top: var(--ha-space-6);
}
ha-md-list {
background: none;
padding: 0;
}
.network-card {
overflow: hidden;
}
@@ -9,9 +9,9 @@ import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import type { InfraredDevice } from "../../../../../data/infrared";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
@@ -82,12 +82,8 @@ export class InfraredConfigDashboard extends LitElement {
${this.hass.localize("ui.panel.config.infrared.my_devices")}
</div>
<div class="card-content network-card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.infrared.my_devices"
)}
>
<ha-list-item-button href="/config/infrared/devices">
<ha-md-list>
<ha-md-list-item type="link" href="/config/infrared/devices">
<ha-svg-icon slot="start" .path=${mdiRemote}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
@@ -96,8 +92,8 @@ export class InfraredConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
</div>
@@ -122,6 +118,11 @@ export class InfraredConfigDashboard extends LitElement {
margin-top: var(--ha-space-6);
}
ha-md-list {
background: none;
padding: 0;
}
.network-card {
overflow: hidden;
}
@@ -2,26 +2,25 @@ import { mdiDelete, mdiLock, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopKeydownEnterSpacePropagation } from "../../../../../common/dom/stop_propagation";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-dialog";
import type {
MatterLockInfo,
MatterLockUser,
} from "../../../../../data/matter-lock";
import {
clearMatterLockUser,
getMatterLockInfo,
getMatterLockUsers,
clearMatterLockUser,
} from "../../../../../data/matter-lock";
import {
showAlertDialog,
@@ -163,10 +162,11 @@ class DialogMatterLockManage extends LitElement {
)}
</p>`
: html`
<ha-list-base>
<ha-md-list>
${occupiedUsers.map(
(user) => html`
<ha-list-item-button
<ha-md-list-item
type="button"
.user=${user}
@click=${this._handleUserClick}
>
@@ -191,12 +191,11 @@ class DialogMatterLockManage extends LitElement {
.path=${mdiDelete}
.user=${user}
@click=${this._handleDeleteUserClick}
@keydown=${stopKeydownEnterSpacePropagation}
></ha-icon-button>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
`
}
${
@@ -3,8 +3,8 @@ import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/item/ha-list-item-button";
import "../../../../../../components/list/ha-list-base";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../../../types";
import type { MatterAddDeviceStep } from "../dialog-matter-add-device";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -23,8 +23,14 @@ class MatterAddDeviceExisting extends LitElement {
</p>
</div>
<ha-list-base>
<ha-list-item-button .step=${"google_home"} @click=${this._onItemClick}>
<ha-md-list>
<ha-md-list-item
interactive
type="button"
.step=${"google_home"}
@click=${this._onItemClick}
@keydown=${this._onItemClick}
>
<img
src="/static/images/logo_google_home.png"
alt=""
@@ -37,8 +43,14 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button .step=${"apple_home"} @click=${this._onItemClick}>
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"apple_home"}
@click=${this._onItemClick}
@keydown=${this._onItemClick}
>
<img
src="/static/images/logo_apple_home.png"
alt=""
@@ -51,8 +63,14 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button .step=${"generic"} @click=${this._onItemClick}>
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"generic"}
@click=${this._onItemClick}
@keydown=${this._onItemClick}
>
<div class="logo" slot="start">
<ha-svg-icon path=${mdiHomeAutomation}></ha-svg-icon>
</div>
@@ -62,12 +80,15 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
`;
}
private _onItemClick(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
const item = ev.currentTarget as any;
const step = item.step as MatterAddDeviceStep;
fireEvent(this, "step-selected", { step });
@@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/item/ha-list-item-button";
import "../../../../../../components/list/ha-list-base";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@@ -18,8 +18,14 @@ class MatterAddDeviceMain extends LitElement {
${this.hass.localize(`ui.dialogs.matter-add-device.main.question`)}
</p>
</div>
<ha-list-base>
<ha-list-item-button .step=${"new"} @click=${this._onItemClick}>
<ha-md-list>
<ha-md-list-item
interactive
type="button"
.step=${"new"}
@click=${this._onItemClick}
@keydown=${this._onItemClick}
>
<span slot="headline">
${this.hass.localize(
`ui.dialogs.matter-add-device.main.answer_new`
@@ -31,8 +37,14 @@ class MatterAddDeviceMain extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button .step=${"existing"} @click=${this._onItemClick}>
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"existing"}
@click=${this._onItemClick}
@keydown=${this._onItemClick}
>
<span slot="headline">
${this.hass.localize(
`ui.dialogs.matter-add-device.main.answer_existing`
@@ -44,12 +56,15 @@ class MatterAddDeviceMain extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
`;
}
private _onItemClick(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
const item = ev.currentTarget as any;
const step = item.step as "new" | "existing";
fireEvent(this, "step-selected", { step });
@@ -23,9 +23,10 @@ export const sharedStyles = css`
cursor: pointer;
text-decoration: underline;
}
ha-list-base {
ha-md-list {
padding: 0;
--ha-row-item-padding-inline: var(--horizontal-padding, 16px);
--md-list-item-leading-space: var(--horizontal-padding, 16px);
--md-list-item-trailing-space: var(--horizontal-padding, 16px);
margin-bottom: 16px;
}
ha-input {
@@ -16,9 +16,9 @@ import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import { fetchMatterNetworkTopology } from "../../../../../data/matter";
@@ -167,12 +167,9 @@ export class MatterConfigDashboard extends LitElement {
</ha-button>
</div>
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.matter.panel.my_network_title"
)}
>
<ha-list-item-button
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
@@ -183,8 +180,9 @@ export class MatterConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
@@ -195,8 +193,8 @@ export class MatterConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -206,12 +204,8 @@ export class MatterConfigDashboard extends LitElement {
return html`
<ha-card class="nav-card">
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.matter.panel.options_title"
)}
>
<ha-list-item-button href="/config/matter/options">
<ha-md-list>
<ha-md-list-item type="link" href="/config/matter/options">
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
@@ -224,10 +218,10 @@ export class MatterConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
${
isComponentLoaded(this.hass.config, "thread")
? html`<ha-list-item-button href="/config/thread">
? html`<ha-md-list-item type="link" href="/config/thread">
<ha-svg-icon
slot="start"
.path=${THREAD_ICON}
@@ -243,10 +237,10 @@ export class MatterConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
</ha-md-list-item>`
: nothing
}
</ha-list-nav>
</ha-md-list>
</div>
</ha-card>
`;
@@ -313,6 +307,15 @@ export class MatterConfigDashboard extends LitElement {
margin-top: var(--ha-space-6);
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
.network-status div.heading {
display: flex;
align-items: center;
@@ -1,11 +1,12 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/item/ha-row-item";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import {
acceptSharedMatterDevice,
canCommissionMatterExternal,
@@ -54,120 +55,122 @@ class MatterOptionsPage extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing
}
${
canCommissionMatterExternal(this.hass)
? html`<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._startMobileCommissioning}
>
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning_action"
)}
</ha-button>
</ha-row-item>`
: nothing
}
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._commission}
>
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device_action"
)}
</ha-button>
</ha-row-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._acceptSharedDevice}
>
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device_action"
)}
</ha-button>
</ha-row-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._setWifi}
>
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials_action"
)}
</ha-button>
</ha-row-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._setThread}
>
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials_action"
)}
</ha-button>
</ha-row-item>
<ha-md-list>
${
canCommissionMatterExternal(this.hass)
? html`<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._startMobileCommissioning}
>
${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning_action"
)}
</ha-button>
</ha-md-list-item>`
: nothing
}
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._commission}
>
${this.hass.localize(
"ui.panel.config.matter.panel.commission_device_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._acceptSharedDevice}
>
${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._setWifi}
>
${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials_description"
)}
</span>
<ha-button
appearance="plain"
slot="end"
size="s"
@click=${this._setThread}
>
${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
</ha-card>
</div>
</hass-subpage>
@@ -320,6 +323,15 @@ class MatterOptionsPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
`,
];
}
@@ -16,11 +16,10 @@ import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import { domainToName } from "../../../../../data/integration";
@@ -188,21 +187,22 @@ export class ModbusConfigDashboard extends LitElement {
// removed while the connection was listed has nowhere to link to
if (!holder.entry) {
return html`
<ha-list-item-base class="holder">
<ha-md-list-item class="holder">
<ha-svg-icon
slot="start"
.path=${source === "yaml" ? mdiFileDocumentOutline : mdiPuzzle}
></ha-svg-icon>
<div slot="headline">${holder.id}</div>
${this._renderUnits(holder)}
</ha-list-item-base>
</ha-md-list-item>
`;
}
const { domain, title } = holder.entry;
return html`
<ha-list-item-button
<ha-md-list-item
type="link"
class="holder"
href=${`/config/integrations/integration/${domain}#config_entry=${holder.id}`}
>
@@ -225,19 +225,19 @@ export class ModbusConfigDashboard extends LitElement {
</div>
${this._renderUnits(holder)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
private _renderSerialPortLink(): TemplateResult {
return html`
<ha-list-item-button class="holder" href="/config/serial">
<ha-md-list-item type="link" class="holder" href="/config/serial">
<ha-svg-icon slot="start" .path=${mdiCableData}></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.modbus.view_serial_port")}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
@@ -256,12 +256,12 @@ export class ModbusConfigDashboard extends LitElement {
private _renderConnectionItem(item: ConnectionListItem): TemplateResult {
return html`
<ha-list-item-base class="connection">
<ha-md-list-item class="connection">
<ha-svg-icon slot="start" .path=${item.icon}></ha-svg-icon>
<div slot="headline">${item.primary}</div>
<div slot="supporting-text">${item.secondary}</div>
${this._renderState(item)}
</ha-list-item-base>
</ha-md-list-item>
${item.holders.map((holder) => this._renderHolder(holder, item.source))}
${
item.serialDevice && isComponentLoaded(this.hass.config, "usb")
@@ -283,9 +283,9 @@ export class ModbusConfigDashboard extends LitElement {
"ui.panel.config.modbus.connections_description"
)}
</div>
<ha-list-base>
<ha-md-list>
${items.map((item) => this._renderConnectionItem(item))}
</ha-list-base>
</ha-md-list>
</div>
</ha-card>
`;
@@ -474,16 +474,23 @@ export class ModbusConfigDashboard extends LitElement {
color: var(--secondary-text-color);
}
ha-list-item-base,
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 0;
ha-md-list {
background: none;
padding: 0;
}
ha-list-item-base.connection:not(:first-child) {
ha-md-list-item {
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-one-line-container-height: 0;
--md-list-item-two-line-container-height: 0;
--md-list-item-three-line-container-height: 0;
}
ha-md-list-item.connection:not(:first-child) {
border-top: 1px solid var(--divider-color);
margin-top: var(--ha-space-2);
--ha-row-item-padding-block: var(--ha-space-4) var(--ha-space-2);
--md-list-item-top-space: var(--ha-space-4);
}
.state {
@@ -505,11 +512,11 @@ export class ModbusConfigDashboard extends LitElement {
background-color: var(--success-color);
}
.holder {
--ha-row-item-padding-inline: var(--ha-space-14) var(--ha-space-4);
ha-md-list-item.holder {
--md-list-item-leading-space: var(--ha-space-14);
}
.holder img[slot="start"] {
ha-md-list-item.holder img[slot="start"] {
width: 24px;
height: 24px;
}
@@ -1,3 +1,7 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
mdiAlertCircleOutline,
mdiCheck,
@@ -5,37 +9,32 @@ import {
mdiShape,
mdiTune,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../../../../../common/decorators/storage";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-code-editor";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-icon-next";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/ha-switch";
import "../../../../../components/input/ha-input";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/list/ha-list-nav";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import { fetchIntegrationManifest } from "../../../../../data/integration";
import type { Action } from "../../../../../data/script";
import { callExecuteScript } from "../../../../../data/service";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import "../../../../../layouts/hass-subpage";
import { mdiMqttLogo } from "../../../../../resources/mqtt-logo-svg";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { showToast } from "../../../../../util/toast";
import "./mqtt-subscribe-card";
import { brandsUrl } from "../../../../../util/brands-url";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import { fetchIntegrationManifest } from "../../../../../data/integration";
import { mdiMqttLogo } from "../../../../../resources/mqtt-logo-svg";
const qosLevel = ["0", "1", "2"];
@@ -187,12 +186,9 @@ export class MQTTConfigPanel extends LitElement {
${this.hass.localize("ui.panel.config.mqtt.my_network_title")}
</div>
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.mqtt.my_network_title"
)}
>
<ha-list-item-button
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
@@ -202,8 +198,9 @@ export class MQTTConfigPanel extends LitElement {
})}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
@@ -213,8 +210,8 @@ export class MQTTConfigPanel extends LitElement {
})}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -224,8 +221,8 @@ export class MQTTConfigPanel extends LitElement {
return html`
<ha-card class="nav-card">
<div class="card-content">
<ha-list-base>
<ha-list-item-button @click=${this._openOptionFlow}>
<ha-md-list>
<ha-md-list-item type="link" @click=${this._openOptionFlow}>
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.mqtt.option_flow")}
@@ -236,8 +233,8 @@ export class MQTTConfigPanel extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button @click=${this._openConfigFlow}>
</ha-md-list-item>
<ha-md-list-item type="link" @click=${this._openConfigFlow}>
<ha-svg-icon slot="start" .path=${mdiMqttLogo}></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.mqtt.config_flow")}
@@ -248,8 +245,8 @@ export class MQTTConfigPanel extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -407,6 +404,13 @@ export class MQTTConfigPanel extends LitElement {
margin: 0 auto var(--ha-space-4);
max-width: 600px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-select {
width: 96px;
margin: 0 8px;
@@ -4,9 +4,9 @@ import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import { UNAVAILABLE } from "../../../../../data/entity/entity";
import { FALLBACK_DOMAIN_ICONS } from "../../../../../data/icons";
import type { RadioFrequencyTransmitter } from "../../../../../data/radio_frequency";
@@ -73,12 +73,11 @@ export class RadioFrequencyConfigDashboard extends LitElement {
<ha-card class="network-card">
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.radio_frequency.title"
)}
>
<ha-list-item-button href="/config/radio-frequency/devices">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/radio-frequency/devices"
>
<ha-svg-icon
slot="start"
.path=${FALLBACK_DOMAIN_ICONS[DOMAIN]}
@@ -90,8 +89,8 @@ export class RadioFrequencyConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
</div>
@@ -112,6 +111,11 @@ export class RadioFrequencyConfigDashboard extends LitElement {
max-width: 600px;
}
ha-md-list {
background: none;
padding: 0;
}
.network-card {
overflow: hidden;
}
@@ -22,11 +22,10 @@ import "../../../../../components/ha-app-icon";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import {
domainToName,
getConfigPanelPath,
@@ -43,13 +42,13 @@ import type {
} from "../../../../../data/usb";
import { listSerialPortsWithUsage } from "../../../../../data/usb";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import { mdiEsphomeLogo } from "../../../../../resources/esphome-logo-svg";
import { showSerialPortInfoDialog } from "./show-dialog-serial-port-info";
import "../../../../../layouts/hass-subpage";
import { panelIsReady } from "../../../../../layouts/panel-ready";
import { mdiEsphomeLogo } from "../../../../../resources/esphome-logo-svg";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { showSerialPortInfoDialog } from "./show-dialog-serial-port-info";
const ESPHOME_HASS_SCHEME = "esphome-hass://";
@@ -271,7 +270,7 @@ export class SerialConfigDashboard extends LitElement {
const href = this._consumerHref(consumer);
return html`
<ha-list-item-button href=${href} class="consumer">
<ha-md-list-item type="link" href=${href} class="consumer">
${
consumer.kind === "config_entry"
? html`<img
@@ -303,13 +302,14 @@ export class SerialConfigDashboard extends LitElement {
}
<div slot="headline">${this._consumerName(consumer)}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
private _renderDiscoveryFlow(flow: SerialPortDiscoveryFlow): TemplateResult {
return html`
<ha-list-item-button
<ha-md-list-item
type="button"
class="consumer"
.flowId=${flow.flow_id}
@click=${this._continueFlow}
@@ -334,13 +334,13 @@ export class SerialConfigDashboard extends LitElement {
})}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
private _renderModbusLink(): TemplateResult {
return html`
<ha-list-item-button class="consumer" href="/config/modbus">
<ha-md-list-item type="link" class="consumer" href="/config/modbus">
<ha-svg-icon
slot="start"
.path=${mdiTransitConnectionVariant}
@@ -349,7 +349,7 @@ export class SerialConfigDashboard extends LitElement {
${this.hass.localize("ui.panel.config.serial.used_by_modbus")}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`;
}
@@ -378,7 +378,7 @@ export class SerialConfigDashboard extends LitElement {
: undefined;
return html`
<ha-list-item-base class="port">
<ha-md-list-item class="port">
<ha-svg-icon
slot="start"
class=${item.port.present ? "" : "disconnected"}
@@ -415,7 +415,7 @@ export class SerialConfigDashboard extends LitElement {
></ha-icon-button>`
: nothing
}
</ha-list-item-base>
</ha-md-list-item>
${item.consumers.map((consumer) => this._renderConsumer(consumer))}
${this._usedByModbus(item.port) ? this._renderModbusLink() : nothing}
${item.discoveryFlows.map((flow) => this._renderDiscoveryFlow(flow))}
@@ -438,9 +438,9 @@ export class SerialConfigDashboard extends LitElement {
<div class="card-header">${header}</div>
<div class="card-content">
<div class="description">${description}</div>
<ha-list-base>
<ha-md-list>
${items.map((item) => this._renderPortItem(item))}
</ha-list-base>
</ha-md-list>
</div>
</ha-card>
`;
@@ -688,28 +688,35 @@ export class SerialConfigDashboard extends LitElement {
color: var(--secondary-text-color);
}
ha-list-item-base,
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 0;
ha-md-list {
background: none;
padding: 0;
}
ha-list-item-base.port:not(:first-child) {
ha-md-list-item {
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-one-line-container-height: 0;
--md-list-item-two-line-container-height: 0;
--md-list-item-three-line-container-height: 0;
}
ha-md-list-item.port:not(:first-child) {
border-top: 1px solid var(--divider-color);
margin-top: var(--ha-space-2);
--ha-row-item-padding-block: var(--ha-space-4) var(--ha-space-2);
--md-list-item-top-space: var(--ha-space-4);
}
ha-list-item-base .disconnected {
ha-md-list-item .disconnected {
color: var(--disabled-text-color);
}
.consumer {
--ha-row-item-padding-inline: var(--ha-space-14) var(--ha-space-4);
ha-md-list-item.consumer {
--md-list-item-leading-space: var(--ha-space-14);
}
.consumer img[slot="start"],
.consumer ha-app-icon[slot="start"] {
ha-md-list-item.consumer img[slot="start"],
ha-md-list-item.consumer ha-app-icon[slot="start"] {
width: 24px;
height: 24px;
}
@@ -16,20 +16,18 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import { animationStyles } from "../../../../../resources/theme/animations.globals";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import { animationStyles } from "../../../../../resources/theme/animations.globals";
import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/list/ha-list-nav";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type {
@@ -221,12 +219,9 @@ class ZHAConfigDashboard extends LitElement {
</ha-button>
</div>
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.zha.configuration_page.my_network_title"
)}
>
<ha-list-item-button
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
@@ -237,8 +232,9 @@ class ZHAConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
@@ -249,8 +245,8 @@ class ZHAConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button href="/config/zha/groups">
</ha-md-list-item>
<ha-md-list-item type="link" href="/config/zha/groups">
<ha-svg-icon
slot="start"
.path=${mdiFolderMultipleOutline}
@@ -275,8 +271,8 @@ class ZHAConfigDashboard extends LitElement {
}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -292,12 +288,8 @@ class ZHAConfigDashboard extends LitElement {
return html`
<ha-card class="nav-card">
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.zha.configuration_page.options_title"
)}
>
<ha-list-item-button href="/config/zha/options">
<ha-md-list>
<ha-md-list-item type="link" href="/config/zha/options">
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
@@ -310,8 +302,8 @@ class ZHAConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button href="/config/zha/network-info">
</ha-md-list-item>
<ha-md-list-item type="link" href="/config/zha/network-info">
<ha-svg-icon
slot="start"
.path=${mdiInformationOutline}
@@ -327,10 +319,13 @@ class ZHAConfigDashboard extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
${dynamicSections.map(
(section) => html`
<ha-list-item-button href=${`/config/zha/section/${section}`}>
<ha-md-list-item
type="link"
href=${`/config/zha/section/${section}`}
>
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
<div slot="headline">
${
@@ -340,10 +335,10 @@ class ZHAConfigDashboard extends LitElement {
}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-nav>
</ha-md-list>
</div>
</ha-card>
`;
@@ -353,8 +348,8 @@ class ZHAConfigDashboard extends LitElement {
return html`
<ha-card class="nav-card">
<div class="card-content">
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zha.configuration_page.download_backup"
@@ -377,8 +372,8 @@ class ZHAConfigDashboard extends LitElement {
"ui.panel.config.zha.configuration_page.download_backup_action"
)}
</ha-progress-button>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zha.configuration_page.migrate_radio"
@@ -399,8 +394,8 @@ class ZHAConfigDashboard extends LitElement {
"ui.panel.config.zha.configuration_page.migrate_radio_action"
)}
</ha-button>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -537,6 +532,15 @@ class ZHAConfigDashboard extends LitElement {
padding: var(--ha-space-12);
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
.network-status div.heading {
display: flex;
align-items: center;
@@ -4,8 +4,8 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { ZHANetworkSettings } from "../../../../../data/zha";
import { fetchZHANetworkSettings } from "../../../../../data/zha";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
@@ -53,8 +53,8 @@ class ZHANetworkInfoPage extends LitElement {
<ha-card>
${
this._networkSettings
? html`<ha-list-base>
<ha-list-item-base>
? html`<ha-md-list>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.channel_label"
@@ -73,16 +73,16 @@ class ZHANetworkInfoPage extends LitElement {
.path=${mdiPencil}
@click=${this._showChannelMigrationDialog}
></ha-icon-button>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">PAN ID</span>
<span slot="supporting-text"
>${
this._networkSettings.settings.network_info.pan_id
}</span
>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Extended PAN ID</span>
<span slot="supporting-text"
>${
@@ -90,14 +90,14 @@ class ZHANetworkInfoPage extends LitElement {
.extended_pan_id
}</span
>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Coordinator IEEE</span>
<span slot="supporting-text"
>${this._networkSettings.settings.node_info.ieee}</span
>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.radio_type"
@@ -106,8 +106,8 @@ class ZHANetworkInfoPage extends LitElement {
<span slot="supporting-text"
>${this._networkSettings.radio_type}</span
>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.serial_port"
@@ -116,12 +116,12 @@ class ZHANetworkInfoPage extends LitElement {
<span slot="supporting-text"
>${this._networkSettings.device.path}</span
>
</ha-list-item-base>
</ha-md-list-item>
${
this._networkSettings.device.baudrate &&
!this._networkSettings.device.path.startsWith("socket://")
? html`
<ha-list-item-base>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.baudrate"
@@ -130,11 +130,11 @@ class ZHANetworkInfoPage extends LitElement {
<span slot="supporting-text"
>${this._networkSettings.device.baudrate}</span
>
</ha-list-item-base>
</ha-md-list-item>
`
: nothing
}
</ha-list-base>`
</ha-md-list>`
: nothing
}
</ha-card>
@@ -174,6 +174,19 @@ class ZHANetworkInfoPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
--md-list-item-supporting-text-size: var(
--md-list-item-label-text-size,
var(--md-sys-typescale-body-large-size, 1rem)
);
}
`,
];
}
@@ -4,13 +4,13 @@ import { customElement, property, state } from "lit/decorators";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import type { ZHAConfiguration } from "../../../../../data/zha";
import {
fetchZHAConfiguration,
@@ -115,8 +115,8 @@ class ZHAOptionsPage extends LitElement {
${
this._configuration
? html`
<ha-list-base>
<ha-list-item-base>
<ha-md-list>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.enable_identify_on_join_label"
@@ -135,8 +135,8 @@ class ZHAOptionsPage extends LitElement {
}
@change=${this._enableIdentifyOnJoinChanged}
></ha-switch>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.default_light_transition_label"
@@ -160,8 +160,8 @@ class ZHAOptionsPage extends LitElement {
>
<span slot="end">s</span>
</ha-input>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.enhanced_light_transition_label"
@@ -180,8 +180,8 @@ class ZHAOptionsPage extends LitElement {
}
@change=${this._enhancedLightTransitionChanged}
></ha-switch>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.light_transitioning_flag_label"
@@ -200,8 +200,8 @@ class ZHAOptionsPage extends LitElement {
}
@change=${this._lightTransitioningFlagChanged}
></ha-switch>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.group_members_assume_state_label"
@@ -220,8 +220,8 @@ class ZHAOptionsPage extends LitElement {
}
@change=${this._groupMembersAssumeStateChanged}
></ha-switch>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.consider_unavailable_mains_label"
@@ -242,11 +242,11 @@ class ZHAOptionsPage extends LitElement {
.options=${this._getUnavailableTimeoutOptions(7200)}
@selected=${this._mainsUnavailableChanged}
></ha-select>
</ha-list-item-base>
</ha-md-list-item>
${
this._customMains
? html`
<ha-list-item-base>
<ha-md-list-item>
<ha-input
slot="end"
type="number"
@@ -261,11 +261,11 @@ class ZHAOptionsPage extends LitElement {
>
<span slot="end">s</span>
</ha-input>
</ha-list-item-base>
</ha-md-list-item>
`
: nothing
}
<ha-list-item-base>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.consider_unavailable_battery_label"
@@ -286,11 +286,11 @@ class ZHAOptionsPage extends LitElement {
.options=${this._getUnavailableTimeoutOptions(21600)}
@selected=${this._batteryUnavailableChanged}
></ha-select>
</ha-list-item-base>
</ha-md-list-item>
${
this._customBattery
? html`
<ha-list-item-base>
<ha-md-list-item>
<ha-input
slot="end"
type="number"
@@ -305,11 +305,11 @@ class ZHAOptionsPage extends LitElement {
>
<span slot="end">s</span>
</ha-input>
</ha-list-item-base>
</ha-md-list-item>
`
: nothing
}
<ha-list-item-base>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.zha.configuration_page.enable_mains_startup_polling_label"
@@ -328,8 +328,8 @@ class ZHAOptionsPage extends LitElement {
}
@change=${this._enableMainsStartupPollingChanged}
></ha-switch>
</ha-list-item-base>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
<div class="card-actions">
<ha-progress-button
appearance="filled"
@@ -463,6 +463,15 @@ class ZHAOptionsPage extends LitElement {
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-select,
ha-input {
min-width: 210px;
@@ -1,12 +1,12 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../../../types";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/item/ha-list-item-button";
import "../../../../../../components/list/ha-list-base";
@customElement("zwave-js-add-node-select-method")
export class ZWaveJsAddNodeSelectMethod extends LitElement {
@@ -26,10 +26,12 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
>`
: nothing
}
<ha-list-base>
<ha-md-list>
${
!this.hideQrWebcam
? html`<ha-list-item-button
? html`<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_webcam"}
.disabled=${!window.isSecureContext}
@@ -45,10 +47,12 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
</ha-md-list-item>`
: nothing
}
<ha-list-item-button
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"qr_code_manual"}
>
@@ -63,8 +67,10 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
@click=${this._selectMethod}
.value=${"search_device"}
>
@@ -79,8 +85,8 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
`;
}
@@ -90,6 +96,12 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
fireEvent(this, "z-wave-method-selected", { method });
}
}
static styles = css`
ha-md-list {
padding: 0;
}
`;
}
declare global {
@@ -2,35 +2,32 @@ import { mdiAccountKey, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import { fireEvent } from "../../../../../common/dom/fire_event";
import {
stopKeydownEnterSpacePropagation,
stopPropagation,
} from "../../../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-dialog";
import type {
ZwaveCredentialCapabilities,
ZwaveUser,
} from "../../../../../data/zwave_js-credentials";
import {
canAddZwaveUser,
compatibleUserTypes,
deleteZwaveAllUsers,
deleteZwaveUser,
getZwaveCredentialCapabilities,
getZwaveUsers,
compatibleUserTypes,
} from "../../../../../data/zwave_js-credentials";
import {
showAlertDialog,
@@ -223,10 +220,11 @@ class DialogZwaveCredentialManage extends LitElement {
)}
</p>`
: html`
<ha-list-base>
<ha-md-list>
${activeUsers.map(
(user) => html`
<ha-list-item-button
<ha-md-list-item
type="button"
data-user-id=${user.user_id}
@click=${this._handleUserClick}
>
@@ -266,12 +264,11 @@ class DialogZwaveCredentialManage extends LitElement {
data-user-id=${user.user_id}
?disabled=${this._busy}
@click=${this._handleDeleteUserClick}
@keydown=${stopKeydownEnterSpacePropagation}
></ha-icon-button>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
`
}
</div>
@@ -8,14 +8,14 @@ import { computeAreaName } from "../../../../../../common/entity/compute_area_na
import { computeDeviceNameDisplay } from "../../../../../../common/entity/compute_device_name";
import { getDeviceArea } from "../../../../../../common/entity/context/get_device_context";
import { caseInsensitiveStringCompare } from "../../../../../../common/string/compare";
import "../../../../../../components/animation/ha-fade-in";
import "../../../../../../components/ha-button";
import "../../../../../../components/ha-dialog";
import "../../../../../../components/ha-dialog-footer";
import "../../../../../../components/ha-domain-icon";
import "../../../../../../components/animation/ha-fade-in";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-spinner";
import "../../../../../../components/item/ha-list-item-base";
import "../../../../../../components/list/ha-list-base";
import {
configEntriesContext,
devicesContext,
@@ -115,13 +115,13 @@ class DialogZWaveJSRebuildNetworkRoutesDetail extends DialogMixin<ZWaveJSRebuild
)}
</p>`
: this._zwaveDevices
? html`<ha-list-base>
? html`<ha-md-list>
${this._filteredDevices(
this._progress,
this._zwaveDevices
).map(
(device) => html`
<ha-list-item-base>
<ha-md-list-item>
<ha-domain-icon
slot="start"
.domain=${device.domain}
@@ -129,10 +129,10 @@ class DialogZWaveJSRebuildNetworkRoutesDetail extends DialogMixin<ZWaveJSRebuild
></ha-domain-icon>
<span slot="headline">${device.name}</span>
<span slot="supporting-text">${device.areaName}</span>
</ha-list-item-base>
</ha-md-list-item>
`
)}
</ha-list-base>`
</ha-md-list>`
: nothing
}
</ha-dialog>
@@ -208,14 +208,16 @@ class DialogZWaveJSRebuildNetworkRoutesDetail extends DialogMixin<ZWaveJSRebuild
static get styles(): CSSResultGroup {
return [
css`
ha-list-base {
--ha-list-gap: var(--ha-space-2);
ha-md-list {
gap: var(--ha-space-2);
min-height: 300px;
}
ha-list-item-base {
--ha-row-item-min-height: 0;
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-padding-inline: var(--ha-space-2);
ha-md-list-item {
--md-list-item-two-line-container-height: 0;
--md-list-item-top-space: var(--ha-space-1);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-leading-space: var(--ha-space-2);
--md-list-item-trailing-space: var(--ha-space-2);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-normal);
border-radius: var(--ha-border-radius-lg);
@@ -23,12 +23,10 @@ import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/list/ha-list-nav";
import "../../../../../components/progress/ha-progress-ring";
import type { ConfigEntry } from "../../../../../data/config_entries";
import {
@@ -307,12 +305,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
</ha-button>
</div>
<div class="card-content network-card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.network_card_title"
)}
>
<ha-list-item-button
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
@@ -323,8 +318,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
@@ -335,10 +331,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
${
this._provisioningEntries?.length
? html`<ha-list-item-button
? html`<ha-md-list-item
type="link"
href=${`provisioned?config_entry=${this.configEntryId}`}
>
<ha-svg-icon slot="start" .path=${mdiQrcode}></ha-svg-icon>
@@ -349,10 +346,10 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
</ha-md-list-item>`
: nothing
}
</ha-list-nav>
</ha-md-list>
</div>
</ha-card>
`;
@@ -362,12 +359,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
return html`
<ha-card class="nav-card">
<div class="card-content">
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.zwave_js.navigation.general"
)}
>
<ha-list-item-button
<ha-md-list>
<ha-md-list-item
type="link"
href=${`options?config_entry=${this.configEntryId}`}
>
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
@@ -382,8 +376,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`statistics?config_entry=${this.configEntryId}`}
>
<ha-svg-icon slot="start" .path=${mdiPoll}></ha-svg-icon>
@@ -398,8 +393,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`logs?config_entry=${this.configEntryId}`}
>
<ha-svg-icon
@@ -417,8 +413,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button href="/config/analytics?section=zwave">
</ha-md-list-item>
<ha-md-list-item type="link" href="/config/analytics?section=zwave">
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
@@ -440,8 +436,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="link"
href=${`network-info?config_entry=${this.configEntryId}`}
>
<ha-svg-icon
@@ -459,8 +456,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-nav>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
@@ -493,8 +490,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
)}
${this._restoreProgress}%
</div>`
: html`<ha-list-base>
<ha-list-item-base>
: html`<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nvm_backup.download_backup"
@@ -519,8 +516,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.zwave_js.dashboard.nvm_backup.download_action"
)}
</ha-button>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nvm_backup.restore_backup"
@@ -548,8 +545,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@change=${this._handleRestoreFileSelected}
style="display: none"
/>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nvm_backup.migrate"
@@ -570,8 +567,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.zwave_js.dashboard.nvm_backup.migrate_action"
)}
</ha-button>
</ha-list-item-base>
</ha-list-base>`
</ha-md-list-item>
</ha-md-list>`
}
</div>
</ha-card>
@@ -908,6 +905,15 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
max-width: 600px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
.network-card .card-header {
display: flex;
align-items: center;
@@ -3,8 +3,8 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { ZWaveJSControllerStatisticsUpdatedMessage } from "../../../../../data/zwave_js";
import { subscribeZwaveControllerStatistics } from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-subpage";
@@ -60,7 +60,7 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
>
<div class="container">
<ha-card>
<ha-list-base>
<ha-md-list>
${this._renderStat("messages_tx")}
${this._renderStat("messages_rx")}
${this._renderStat("messages_dropped_tx")}
@@ -69,7 +69,7 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
${this._renderStat("timeout_ack")}
${this._renderStat("timeout_response")}
${this._renderStat("timeout_callback")}
</ha-list-base>
</ha-md-list>
</ha-card>
</div>
</hass-subpage>
@@ -78,7 +78,7 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
private _renderStat(key: string) {
return html`
<ha-list-item-base>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.statistics.${key}.label`
@@ -90,7 +90,7 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
)}
</span>
<span slot="end">${this._statistics?.[key] ?? 0}</span>
</ha-list-item-base>
</ha-md-list-item>
`;
}
@@ -104,6 +104,15 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
max-width: 600px;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
span[slot="end"] {
font-size: 0.95em;
color: var(--primary-text-color);
@@ -2,8 +2,8 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { ZWaveJSNetwork } from "../../../../../data/zwave_js";
import { fetchZwaveNetworkStatus } from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-subpage";
@@ -49,8 +49,8 @@ class ZWaveJSNetworkInfoPage extends LitElement {
<ha-card>
${
this._network
? html`<ha-list-base>
<ha-list-item-base>
? html`<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
@@ -59,8 +59,8 @@ class ZWaveJSNetworkInfoPage extends LitElement {
<span slot="supporting-text">
${formatHomeIdAsHex(this._network.controller.home_id)}
</span>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
@@ -69,8 +69,8 @@ class ZWaveJSNetworkInfoPage extends LitElement {
<span slot="supporting-text">
${this._network.client.driver_version}
</span>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
@@ -79,8 +79,8 @@ class ZWaveJSNetworkInfoPage extends LitElement {
<span slot="supporting-text">
${this._network.client.server_version}
</span>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
@@ -89,8 +89,8 @@ class ZWaveJSNetworkInfoPage extends LitElement {
<span slot="supporting-text">
${this._network.client.ws_server_url}
</span>
</ha-list-item-base>
</ha-list-base>`
</ha-md-list-item>
</ha-md-list>`
: nothing
}
</ha-card>
@@ -111,6 +111,19 @@ class ZWaveJSNetworkInfoPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
--md-list-item-supporting-text-size: var(
--md-list-item-label-text-size,
var(--md-sys-typescale-body-large-size, 1rem)
);
}
`,
];
}
@@ -3,8 +3,8 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type {
ZWaveJSClient,
ZWaveJSNetwork,
@@ -61,8 +61,8 @@ class ZWaveJSOptionsPage extends LitElement {
<ha-card>
${
this._network
? html`<ha-list-base>
<ha-list-item-base>
? html`<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.common.rebuild_network_routes"
@@ -84,8 +84,8 @@ class ZWaveJSOptionsPage extends LitElement {
"ui.panel.config.zwave_js.dashboard.rebuild_routes_action"
)}
</ha-button>
</ha-list-item-base>
<ha-list-item-base>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
@@ -113,8 +113,8 @@ class ZWaveJSOptionsPage extends LitElement {
"ui.panel.config.zwave_js.dashboard.remove_node_action"
)}
</ha-button>
</ha-list-item-base>
</ha-list-base>`
</ha-md-list-item>
</ha-md-list>`
: nothing
}
</ha-card>
@@ -149,6 +149,15 @@ class ZWaveJSOptionsPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
`,
];
}
@@ -4,11 +4,12 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-switch";
import "../../../components/ha-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
@@ -157,23 +158,25 @@ export class DialogLabsPreviewFeatureEnable
${
createBackupTexts
? html`
<ha-row-item>
<span slot="headline">${createBackupTexts.title}</span>
${
createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing
}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
></ha-switch>
</ha-row-item>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${
createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing
}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing
}
@@ -208,8 +211,10 @@ export class DialogLabsPreviewFeatureEnable
color: var(--secondary-text-color);
}
ha-row-item {
--ha-row-item-padding-inline: var(--ha-space-6);
ha-md-list {
background: none;
--md-list-item-leading-space: var(--ha-space-6);
--md-list-item-trailing-space: var(--ha-space-6);
margin: 0;
padding: 0;
border-top: var(--ha-border-width-sm) solid var(--divider-color);
@@ -2,11 +2,11 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-nav";
import "../../../layouts/hass-subpage";
import "../../../components/ha-card";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-icon-next";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-http-form";
import "./ha-config-network";
@@ -57,14 +57,10 @@ class HaConfigSectionNetwork extends LitElement {
"ui.panel.config.network.discovery.title"
)}
>
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.network.discovery.title"
)}
>
<ha-md-list>
${NETWORK_BROWSERS.map(
(domain) => html`
<ha-list-item-button href="/config/${domain}">
<ha-md-list-item type="link" href="/config/${domain}">
<div slot="headline">
${this.hass.localize(
`ui.panel.config.network.discovery.${domain}`
@@ -76,10 +72,10 @@ class HaConfigSectionNetwork extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-nav>
</ha-md-list>
</ha-card>
`
: ""
@@ -106,6 +102,10 @@ class HaConfigSectionNetwork extends LitElement {
margin-bottom: 24px;
max-width: 600px;
}
.discovery-card ha-md-list {
background: none;
padding-top: 0;
}
`;
}
@@ -7,12 +7,12 @@ import { isIPAddress } from "../../../common/string/is_ip_address";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { HaInput } from "../../../components/input/ha-input";
import "../../../components/input/ha-input-copy";
import type { HaInputCopy } from "../../../components/input/ha-input-copy";
import "../../../components/item/ha-row-item";
import type { CloudStatus } from "../../../data/cloud";
import { fetchCloudStatus } from "../../../data/cloud";
import { saveCoreConfig } from "../../../data/core";
@@ -161,7 +161,7 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
${
hasCloud
? html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.url.external_use_ha_cloud"
@@ -173,7 +173,7 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
.checked=${this._cloudChecked}
@change=${this._toggleCloud}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`
: nothing
}
@@ -261,7 +261,7 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
<h4>
${this.hass.localize("ui.panel.config.url.internal_url_label")}
</h4>
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.url.internal_url_automatic"
@@ -278,7 +278,7 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
.checked=${!this._showCustomInternalUrl}
@change=${this._toggleInternalAutomatic}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
<div class="url-container">
<ha-input-copy
@@ -466,10 +466,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
flex: 1;
}
ha-row-item {
--ha-row-item-padding-block: 0;
--ha-row-item-padding-inline: 0;
--ha-row-item-min-height: 48px;
ha-md-list-item {
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 48px;
}
.no-wrap {
@@ -3,8 +3,8 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-card";
import "../../../components/ha-list";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type {
IntegrationManifest,
IntegrationSetup,
@@ -40,7 +40,7 @@ class IntegrationsStartupTime extends LitElement {
}
return html`
<ha-list-base>
<ha-md-list>
${this._setups?.map((setup) => {
const manifest = this._manifests && this._manifests[setup.domain];
const docLink =
@@ -55,7 +55,7 @@ class IntegrationsStartupTime extends LitElement {
const setupSeconds = setup.seconds?.toFixed(2);
return html`
<ha-list-item-button .href=${docLink} target="_blank">
<ha-md-list-item .href=${docLink} type="link" target="_blank">
<img
alt=""
loading="lazy"
@@ -71,17 +71,17 @@ class IntegrationsStartupTime extends LitElement {
referrerpolicy="no-referrer"
slot="start"
/>
<span slot="headline">
<span>
${domainToName(this.hass.localize, setup.domain, manifest)}
</span>
<span slot="supporting-text">${setup.domain}</span>
<div slot="end">
${setupSeconds ? html`${setupSeconds} s` : ""}
</div>
</ha-list-item-button>
</ha-md-list-item>
`;
})}
</ha-list-base>
</ha-md-list>
`;
}
@@ -21,15 +21,14 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-list-item";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/item/ha-list-item-button";
import "../../../components/item/ha-row-item";
import "../../../components/list/ha-list-base";
import "../../../components/voice-assistant-brand-icon";
import type { AssistPipeline } from "../../../data/assist_pipeline";
import {
@@ -53,6 +52,7 @@ import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/sh
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("assist-pref")
export class AssistPref extends LitElement {
@@ -123,14 +123,17 @@ export class AssistPref extends LitElement {
class="icon-link"
></ha-icon-button>
</div>
<ha-list-base>
<ha-list>
${this._pipelines.map(
(pipeline) => html`
<ha-list-item-button
<ha-list-item
twoline
hasMeta
role="button"
.id=${pipeline.id}
@click=${this._editPipeline}
>
<span slot="headline">
<span>
${pipeline.name}
${
this._preferred === pipeline.id
@@ -138,14 +141,13 @@ export class AssistPref extends LitElement {
: ""
}
</span>
<span slot="supporting-text">
<span slot="secondary">
${formatLanguageCode(pipeline.language, this.hass.locale)}
</span>
<ha-dropdown
slot="end"
slot="meta"
placement="bottom-end"
@click=${stopPropagation}
@keydown=${this._handleDropdownKeydown}
@wa-select=${this._handlePipelineMenuAction}
>
<ha-icon-button
@@ -197,10 +199,10 @@ export class AssistPref extends LitElement {
<ha-svg-icon slot="icon" .path=${mdiTrashCan}></ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
</ha-list-item-button>
</ha-list-item>
`
)}
</ha-list-base>
</ha-list>
<ha-button
appearance="filled"
@click=${this._addPipeline}
@@ -212,7 +214,7 @@ export class AssistPref extends LitElement {
)}
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.voice_assistants.expose.expose_new_entities"
@@ -229,7 +231,7 @@ export class AssistPref extends LitElement {
.disabled=${this._exposeNew === undefined}
@change=${this._exposeNewToggleChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
<div class="card-actions">
<ha-button
appearance="plain"
@@ -405,12 +407,6 @@ export class AssistPref extends LitElement {
});
}
private _handleDropdownKeydown(ev: KeyboardEvent) {
if (ev.key === " " || ev.key === "Enter") {
ev.stopPropagation();
}
}
static styles = css`
a {
color: var(--primary-color);
@@ -432,7 +428,14 @@ export class AssistPref extends LitElement {
direction: var(--direction);
color: var(--secondary-text-color);
}
ha-list-item-button span ha-svg-icon {
ha-list-item {
--mdc-list-item-meta-size: auto;
--mdc-list-item-meta-display: flex;
--mdc-list-side-padding-right: 8px;
--mdc-list-side-padding-left: 16px;
}
ha-list-item span ha-svg-icon {
color: currentColor;
width: 16px;
}
@@ -5,12 +5,11 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/voice-assistant-brand-icon";
import type { CloudStatusLoggedIn } from "../../../data/cloud";
import { updateCloudPref } from "../../../data/cloud";
import type { ExposeEntitySettings } from "../../../data/expose";
@@ -19,6 +18,7 @@ import {
setExposeNewEntities,
} from "../../../data/expose";
import type { HomeAssistant } from "../../../types";
import "../../../components/voice-assistant-brand-icon";
@customElement("cloud-alexa-pref")
export class CloudAlexaPref extends LitElement {
@@ -139,7 +139,7 @@ export class CloudAlexaPref extends LitElement {
</ha-alert>`
: nothing
}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.expose_new_entities"
@@ -156,11 +156,11 @@ export class CloudAlexaPref extends LitElement {
.disabled=${this._exposeNew === undefined}
@change=${this._exposeNewToggleChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
alexa_registered
? html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_state_reporting"
@@ -176,7 +176,7 @@ export class CloudAlexaPref extends LitElement {
.checked=${alexa_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`
: nothing
}
@@ -262,8 +262,10 @@ export class CloudAlexaPref extends LitElement {
a {
color: var(--primary-color);
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
.header-actions {
position: absolute;
@@ -5,14 +5,13 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/input/ha-input";
import type { HaInput } from "../../../components/input/ha-input";
import "../../../components/item/ha-row-item";
import "../../../components/voice-assistant-brand-icon";
import type { CloudStatusLoggedIn } from "../../../data/cloud";
import { updateCloudPref } from "../../../data/cloud";
import type { ExposeEntitySettings } from "../../../data/expose";
@@ -22,6 +21,7 @@ import {
} from "../../../data/expose";
import type { HomeAssistant } from "../../../types";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
import "../../../components/voice-assistant-brand-icon";
@customElement("cloud-google-pref")
export class CloudGooglePref extends LitElement {
@@ -138,7 +138,7 @@ export class CloudGooglePref extends LitElement {
`
: nothing
}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.cloud.account.google.expose_new_entities"
@@ -155,7 +155,7 @@ export class CloudGooglePref extends LitElement {
.disabled=${this._exposeNew === undefined}
@change=${this._exposeNewToggleChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
google_registered
? html`
@@ -183,7 +183,7 @@ export class CloudGooglePref extends LitElement {
`
: nothing
}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass!.localize(
"ui.panel.config.cloud.account.google.enable_state_reporting"
@@ -199,9 +199,9 @@ export class CloudGooglePref extends LitElement {
.checked=${google_report_state}
@change=${this._reportToggleChanged}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.google.security_devices"
@@ -213,7 +213,7 @@ export class CloudGooglePref extends LitElement {
)}</span
>
<ha-switch slot="end"></ha-switch>
</ha-row-item>
</ha-md-list-item>
<ha-input
id="google_secure_devices_pin"
@@ -344,8 +344,10 @@ export class CloudGooglePref extends LitElement {
direction: var(--direction);
color: var(--secondary-text-color);
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
ha-input {
width: 250px;
@@ -17,8 +17,8 @@ import {
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-checkbox";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/voice-assistant-brand-icon";
import { fetchCloudAlexaEntity } from "../../../data/alexa";
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
@@ -190,7 +190,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
const exposedToAssist = this.exposed.conversation;
return html`
<ha-row-item>
<ha-md-list-item>
<h3 slot="headline">
${this.hass.localize("ui.dialogs.voice-settings.expose_header")}
</h3>
@@ -200,7 +200,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
.assistants=${uiAssistants}
.checked=${anyExposed}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${
anyExposed
? showAssistants.map((key) => {
@@ -224,7 +224,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
this._googleEntity?.might_2fa;
return html`
<ha-row-item>
<ha-md-list-item>
<voice-assistant-brand-icon
slot="start"
.voiceAssistantId=${key}
@@ -274,7 +274,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
.disabled=${manualConfig || (!exposed && !supported)}
.checked=${exposed}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`;
})
: nothing
@@ -321,7 +321,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
)}
</ha-alert>`
: html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline">
${
this.hass.states[this.entityId]
@@ -339,7 +339,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
.checked=${(this._aliases ?? this.entry.aliases).includes(null)}
@change=${this._toggleEntityNameAlias}
></ha-switch>
</ha-row-item>
</ha-md-list-item>
<ha-aliases-editor
.aliases=${(this._aliases ?? this.entry.aliases).filter(
(a): a is string => a !== null
@@ -445,8 +445,10 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
margin: 32px;
margin-top: 0;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
img {
height: 32px;
+171 -47
View File
@@ -22,7 +22,12 @@ import type {
MarkerLocation,
} from "../../../components/map/ha-locations-editor";
import { saveCoreConfig } from "../../../data/core";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../../data/entity/entity_registry";
import {
HOME_ZONE_ENTITY_ID,
zoneColor,
} from "../../../common/map/entity-map-colors";
import type {
HomeZoneMutableParams,
Zone,
@@ -47,6 +52,19 @@ import { configSections } from "../config-sections";
import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail";
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
interface PendingEdit {
latitude?: number;
longitude?: number;
radius?: number;
}
// How close the saved value must come to a pending one to count as saved
const PENDING_TOLERANCE: Record<keyof PendingEdit, number> = {
latitude: 1e-7,
longitude: 1e-7,
radius: 0.5,
};
@customElement("ha-config-zone")
export class HaConfigZone extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -61,21 +79,36 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
@state() private _stateItems?: HassEntity[];
// Values dragged on the map, shown until the saved data reflects them, so
// a re-render while the save is in flight does not move the marker back.
// A failed save drops them and the marker returns to the saved values.
@state() private _pendingEdits: Record<string, PendingEdit> = {};
@state() private _canEditCore = false;
@query("ha-locations-editor") private _map?: HaLocationsEditor;
private _regEntities: string[] = [];
// Registry creation order decides the zone colors
@state() private _entityReg: EntityRegistryEntry[] = [];
// Storage zone id (its unique id) to entity id
@state() private _zoneEntityIds: Record<string, string> = {};
// Bumped when entity map colors change to recompute the memoized locations
@state() private _colorVersion = 0;
private _getZones = memoizeOne(
(storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => {
(
storageItems: Zone[],
stateItems: HassEntity[],
zoneEntityIds: Record<string, string>,
pendingEdits: Record<string, PendingEdit>,
entityReg: EntityRegistryEntry[],
_colorVersion: number
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
const passiveRadiusColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
const homeRadiusColor =
computedStyles.getPropertyValue("--primary-color");
const stateLocations: MarkerLocation[] = stateItems.map(
(entityState) => ({
@@ -85,21 +118,28 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
latitude: entityState.attributes.latitude,
longitude: entityState.attributes.longitude,
radius: entityState.attributes.radius,
radius_color:
entityState.entity_id === "zone.home"
? homeRadiusColor
: entityState.attributes.passive
? passiveRadiusColor
: zoneRadiusColor,
...pendingEdits[entityState.entity_id],
radius_color: zoneColor(
entityState.entity_id,
!!entityState.attributes.passive,
entityReg,
computedStyles
),
location_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
entityState.entity_id === HOME_ZONE_ENTITY_ID && this._canEditCore,
radius_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
entityState.entity_id === HOME_ZONE_ENTITY_ID && this._canEditCore,
})
);
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
...zone,
radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor,
...pendingEdits[zone.id],
radius_color: zoneColor(
zoneEntityIds[zone.id] ?? `zone.${zone.id}`,
!!zone.passive,
entityReg,
computedStyles
),
location_editable: true,
radius_editable: true,
}));
@@ -110,9 +150,18 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entityReg = entities;
this._regEntities = entities.map(
(registryEntry) => registryEntry.entity_id
);
this._zoneEntityIds = Object.fromEntries(
entities
.filter((registryEntry) => registryEntry.platform === "zone")
.map((registryEntry) => [
registryEntry.unique_id,
registryEntry.entity_id,
])
);
this._filterStates();
}),
];
@@ -265,7 +314,11 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
<ha-locations-editor
.locations=${this._getZones(
this._storageItems,
this._stateItems
this._stateItems,
this._zoneEntityIds,
this._pendingEdits,
this._entityReg,
this._colorVersion
)}
@location-updated=${this._locationUpdated}
@radius-updated=${this._radiusUpdated}
@@ -295,7 +348,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}
}
protected updated() {
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (
!this.route.path.startsWith("/edit/") ||
!this._stateItems ||
@@ -313,11 +367,99 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}
public willUpdate(changedProps: PropertyValues<this>) {
super.updated(changedProps);
super.willUpdate(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._stateItems) {
this._getStates(oldHass);
}
// Zone colors come from theme variables
if (oldHass && oldHass.themes !== this.hass.themes) {
this._colorVersion++;
}
this._settlePendingEdits();
}
// A pending edit is done once the saved data carries its values
private _settlePendingEdits() {
for (const [id, pending] of Object.entries(this._pendingEdits)) {
const saved =
this._storageItems?.find((zone) => zone.id === id) ??
this.hass.states[id]?.attributes;
if (
saved &&
(Object.keys(pending) as (keyof PendingEdit)[]).every(
(key) =>
Math.abs((saved[key] as number) - pending[key]!) <
PENDING_TOLERANCE[key]
)
) {
this._dropPendingEdit(id);
}
}
}
private _dropPendingEdit(id: string) {
const { [id]: _done, ...rest } = this._pendingEdits;
this._pendingEdits = rest;
}
// Saves for one zone run in order, so each sees the entry the previous one
// produced and a failure only drops the values its own request carried
private _saveQueue: Record<string, Promise<void>> = {};
private _saveEdit(id: string, pending: PendingEdit): Promise<void> {
this._pendingEdits = {
...this._pendingEdits,
[id]: { ...this._pendingEdits[id], ...pending },
};
const save = (this._saveQueue[id] ?? Promise.resolve())
.then(() => this._performSave(id, pending))
.finally(() => {
// Only the last save in the chain removes the queue entry
if (this._saveQueue[id] === save) {
delete this._saveQueue[id];
}
});
this._saveQueue[id] = save;
return save;
}
private async _performSave(id: string, pending: PendingEdit) {
try {
if (id === HOME_ZONE_ENTITY_ID) {
await saveCoreConfig(this.hass, pending);
return;
}
const entry = this._storageItems!.find((item) => item.id === id);
if (entry) {
await this._updateEntry(entry, pending);
}
} catch (err: any) {
// The saved values are the truth again for what this request changed;
// a later edit of other values stays pending for its own save
this._dropPendingValues(id, pending);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
text: err.message,
});
}
}
private _dropPendingValues(id: string, failed: PendingEdit) {
const current = this._pendingEdits[id];
if (!current) {
return;
}
const rest = Object.fromEntries(
Object.entries(current).filter(
([key, value]) => failed[key as keyof PendingEdit] !== value
)
) as PendingEdit;
if (Object.keys(rest).length) {
this._pendingEdits = { ...this._pendingEdits, [id]: rest };
} else {
this._dropPendingEdit(id);
}
}
private async _fetchData() {
@@ -359,37 +501,19 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}
}
private async _locationUpdated(ev: CustomEvent) {
if (ev.detail.id === "zone.home" && this._canEditCore) {
await saveCoreConfig(this.hass, {
latitude: ev.detail.location[0],
longitude: ev.detail.location[1],
});
return;
}
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
if (!entry) {
return;
}
this._updateEntry(entry, {
private _locationUpdated(ev: CustomEvent) {
this._saveEdit(ev.detail.id, {
latitude: ev.detail.location[0],
longitude: ev.detail.location[1],
});
}
private async _radiusUpdated(ev: CustomEvent) {
if (ev.detail.id === "zone.home" && this._canEditCore) {
await saveCoreConfig(this.hass, {
radius: Math.round(ev.detail.radius),
});
return;
}
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
if (!entry) {
return;
}
this._updateEntry(entry, {
radius: ev.detail.radius,
private _radiusUpdated(ev: CustomEvent) {
this._saveEdit(ev.detail.id, {
radius:
ev.detail.id === HOME_ZONE_ENTITY_ID
? Math.round(ev.detail.radius)
: ev.detail.radius,
});
}
@@ -416,7 +540,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
const entryId: string = (ev.currentTarget! as any).value;
if (this.narrow && entryId === "zone.home") {
if (this.narrow && entryId === HOME_ZONE_ENTITY_ID) {
this._editHomeZone(ev);
return;
}
@@ -475,7 +599,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
longitude: values.longitude,
radius: values.radius,
});
this._zoomZone("zone.home");
this._zoomZone(HOME_ZONE_ENTITY_ID);
}
private async _updateEntry(
+57 -16
View File
@@ -8,8 +8,13 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getColorByIndex } from "../../../common/color/colors";
import type { ContextType } from "@lit/context";
import { consume, ContextConsumer } from "@lit/context";
import { resolveThemeColor } from "../../../common/color/compute-color";
import {
entityMapColor,
zoneColor,
} from "../../../common/map/entity-map-colors";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@@ -31,6 +36,10 @@ import type {
import type { MapLatLng } from "../../../common/map/map-engine";
import type { HistoryStates } from "../../../data/history";
import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
import type { Themes } from "../../../data/ws-themes";
import { fullEntitiesContext, uiContext } from "../../../data/context";
import { transform } from "../../../common/decorators/transform";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import {
@@ -58,6 +67,18 @@ interface GeoEntity {
@customElement("hui-map-card")
class HuiMapCard extends LitElement implements LovelaceCard {
constructor() {
super();
new ContextConsumer(this, {
context: fullEntitiesContext,
subscribe: true,
callback: (entries) => {
this._entityReg = entries;
this._mapEntities = this._getMapEntities();
},
});
}
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public layout?: string;
@@ -76,12 +97,19 @@ class HuiMapCard extends LitElement implements LovelaceCard {
private _filteredMapEntities: HaMapEntity[] = [];
private _colorDict: Record<string, string> = {};
private _colorIndex = 0;
@state() private _error?: { code: string; message: string };
// Registry creation order decides the palette colors
@state() private _entityReg: EntityRegistryEntry[] = [];
// Palette colors are read from the theme when the entities are built
@state()
@consume({ context: uiContext, subscribe: true })
@transform<ContextType<typeof uiContext>, Themes>({
transformer: ({ themes }) => themes,
})
private _themes?: Themes;
@state() private _clusterMarkers = true;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
@@ -213,7 +241,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
<ha-map
.entities=${this._filteredMapEntities}
.zoom=${this._config.default_zoom ?? DEFAULT_ZOOM}
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
.paths=${this._getHistoryPaths(
this._config,
this._stateHistory,
this._entityReg,
this._themes
)}
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones || false}
.themeMode=${themeMode}
@@ -321,6 +354,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
) {
this._mapEntities = this._getMapEntities();
}
// Private state is not in keyof this
if ((changedProps as PropertyValues).has("_themes") && this.hasUpdated) {
this._mapEntities = this._getMapEntities();
}
// Filter entities by conditions
if (this._config?.conditions && this._mapEntities) {
@@ -432,18 +469,19 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._clusterMarkers = !this._clusterMarkers;
}
// The same color for an entity on every map; a config color still wins
private _getColor(entityId: string): string {
let color = this._colorDict[entityId];
if (color) {
return color;
}
const computedStyles = getComputedStyle(this);
color = getColorByIndex(this._colorIndex, computedStyles);
if (color) {
this._colorIndex++;
this._colorDict[entityId] = color;
if (computeDomain(entityId) === "zone") {
const stateObj = this.hass?.states[entityId];
return zoneColor(
entityId,
!!stateObj?.attributes.passive,
this._entityReg,
computedStyles
);
}
return color;
return entityMapColor(entityId, this._entityReg, computedStyles);
}
private _getSourceEntities(states?: HassEntities): GeoEntity[] {
@@ -503,7 +541,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
private _getHistoryPaths = memoizeOne(
(
config: MapCardConfig,
history?: HistoryStates
history: HistoryStates | undefined,
// Trail colors follow the registry order and the theme like the markers
_entityReg: EntityRegistryEntry[],
_themes: Themes | undefined
): HaMapPaths[] | undefined => {
if (!history || !(config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW)) {
return undefined;
@@ -12,11 +12,11 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-slider";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import "../../../../components/item/ha-row-item";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles";
@@ -145,7 +145,7 @@ export class HuiCardLayoutEditor extends LitElement {
.columnMax=${gridOptions.max_columns}
.step=${this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER}
></ha-grid-size-picker>
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.auto_height"
@@ -162,8 +162,8 @@ export class HuiCardLayoutEditor extends LitElement {
.checked=${options.rows === "auto"}
name="auto-height"
></ha-switch>
</ha-row-item>
<ha-row-item>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width"
@@ -180,8 +180,8 @@ export class HuiCardLayoutEditor extends LitElement {
.checked=${options.columns === "full"}
name="full-width"
></ha-switch>
</ha-row-item>
<ha-row-item>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precise_mode"
@@ -198,7 +198,7 @@ export class HuiCardLayoutEditor extends LitElement {
.checked=${this._preciseMode}
name="precise-mode"
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`
}
`;
@@ -4,11 +4,11 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/list/ha-list-base";
import "../../../../components/user/ha-user-badge";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import type {
LovelaceViewConfig,
ShowViewConfig,
@@ -66,10 +66,10 @@ export class HuiViewVisibilityEditor extends LitElement {
"ui.panel.lovelace.editor.edit_view.visibility.select_users"
)}
</p>
<ha-list-base>
<ha-md-list>
${this._sortedUsers(this._users).map(
(user) => html`
<ha-list-item-base>
<ha-md-list-item>
<ha-user-badge slot="start" .user=${user}></ha-user-badge>
<span slot="headline">${user.name}</span>
<ha-switch
@@ -78,10 +78,10 @@ export class HuiViewVisibilityEditor extends LitElement {
@change=${this._valChange}
.checked=${this.checkUser(user.id)}
></ha-switch>
</ha-list-item-base>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
`;
}
@@ -135,10 +135,15 @@ export class HuiViewVisibilityEditor extends LitElement {
:host {
display: block;
}
ha-list-item-base {
--ha-row-item-padding-inline: 0;
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-min-height: 48px;
ha-md-list {
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-top-space: var(--ha-space-1);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-one-line-container-height: 48px;
}
`;
}
+3 -48
View File
@@ -18,10 +18,7 @@ import {
subscribeFrontendUserData,
} from "../data/frontend";
import { forwardHaptic } from "../data/haptics";
import {
getServiceCallEntityIds,
serviceCallWillDisconnect,
} from "../data/service";
import { serviceCallWillDisconnect } from "../data/service";
import {
DateFormat,
FirstWeekday,
@@ -35,12 +32,7 @@ import { preserveUnchangedRecord } from "../common/util/preserve-unchanged-recor
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
import { subscribePanels } from "../data/ws-panels";
import { translationMetadata } from "../resources/translations-metadata";
import type {
Constructor,
HomeAssistant,
ServiceCallRequest,
ServiceCallResponse,
} from "../types";
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
import {
addBrandsAuth,
clearBrandsTokenRefresh,
@@ -122,7 +114,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
);
}
try {
const response = (await callService(
return (await callService(
conn,
domain,
service,
@@ -130,24 +122,11 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
target,
returnResponse
)) as ServiceCallResponse;
this._reportEntityControlToExternalApp(
domain,
service,
serviceData,
target
);
return response;
} catch (err: any) {
if (
err.error?.code === ERR_CONNECTION_LOST &&
serviceCallWillDisconnect(domain, service, serviceData)
) {
this._reportEntityControlToExternalApp(
domain,
service,
serviceData,
target
);
return { context: { id: "" } };
}
if (this.hass?.debugConnection) {
@@ -426,28 +405,4 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
this._updateHass({});
}
}
private _reportEntityControlToExternalApp(
domain: string,
service: string,
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"]
) {
const external = this.hass?.auth.external;
if (!external) {
return;
}
const entityIds = getServiceCallEntityIds(serviceData, target);
if (!entityIds.length) {
return;
}
try {
external.fireMessage({
type: "entity/controlled",
payload: { entity_ids: entityIds, domain, service },
});
} catch (_err) {
// Reporting is best effort and must not fail the service call.
}
}
};
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
import {
entityMapColor,
HOME_ZONE_ENTITY_ID,
zoneColor,
} from "../../../src/common/map/entity-map-colors";
// Palette slot N reads back as "color-N", so an index is visible in the result
const styles = {
getPropertyValue: (name: string) => name.replace("--", ""),
} as unknown as CSSStyleDeclaration;
const entry = (entityId: string, createdAt: number, id = entityId) =>
({
entity_id: entityId,
created_at: createdAt,
id,
}) as EntityRegistryEntry;
describe("entity map colors", () => {
it("colors zones, persons and trackers in one creation order", () => {
const entries = [
entry("zone.work", 40),
entry("zone.school", 10),
entry("person.anne", 20),
entry("device_tracker.phone", 30),
entry("light.kitchen", 25),
];
expect(entityMapColor("zone.school", entries, styles)).toBe("color-1");
// A person takes the next slot, never a zone's color
expect(entityMapColor("person.anne", entries, styles)).toBe("color-2");
expect(entityMapColor("device_tracker.phone", entries, styles)).toBe(
"color-3"
);
expect(entityMapColor("zone.work", entries, styles)).toBe("color-4");
});
it("breaks creation ties by registry id", () => {
const entries = [entry("zone.b", 10, "b"), entry("zone.a", 10, "a")];
expect(entityMapColor("zone.a", entries, styles)).toBe("color-1");
expect(entityMapColor("zone.b", entries, styles)).toBe("color-2");
});
it("keeps the home zone out of the palette", () => {
const entries = [entry(HOME_ZONE_ENTITY_ID, 10), entry("zone.work", 20)];
expect(entityMapColor("zone.work", entries, styles)).toBe("color-1");
expect(zoneColor(HOME_ZONE_ENTITY_ID, false, entries, styles)).toBe(
"primary-color"
);
});
it("mutes passive zones", () => {
const entries = [entry("zone.quiet", 10)];
expect(zoneColor("zone.quiet", true, entries, styles)).toBe(
"secondary-text-color"
);
expect(zoneColor("zone.quiet", false, entries, styles)).toBe("color-1");
});
it("gives entities outside the registry a stable color", () => {
const entries = [entry("zone.work", 10)];
const color = entityMapColor("zone.yaml_zone", entries, styles);
expect(entityMapColor("zone.yaml_zone", entries, styles)).toBe(color);
expect(entityMapColor("zone.other_yaml_zone", entries, styles)).not.toBe(
color
);
});
});
+6 -10
View File
@@ -1030,10 +1030,7 @@ describe("MapLibreMapEngine", () => {
expect(handle.radius).toBeCloseTo(110, 5);
element.dispatchEvent(new KeyboardEvent("keyup", { key: "ArrowUp" }));
// The host echoes the old radius once before its save returns
handle.update([52, 4], 100);
expect(handle.radius).toBeCloseTo(110, 5);
// A later identical update is a real change
// Once committed, the host is the truth again
handle.update([52, 4], 100);
expect(handle.radius).toBe(100);
});
@@ -1100,25 +1097,24 @@ describe("MapLibreMapEngine", () => {
expect(element.getAttribute("aria-valuenow")).toBe("150000");
});
it("ignores one update echoing the values from before a drag", async () => {
it("ignores host updates only while a drag is in progress", async () => {
const { engine, ready } = await createEngine();
await ready;
const { handle, center } = addCircle(engine);
center.lngLat = [4.01, 52];
center.fire("dragstart");
// Nothing moves while a drag is in progress
// The user's hand wins while dragging
handle.update([53, 5], 500);
expect(handle.center).toEqual([52, 4]);
center.fire("drag");
center.fire("dragend");
// The host saves and echoes the old values once; that is not a move back
handle.update([52, 4], 100);
expect(handle.center).toEqual([52, 4.01]);
// A later identical update is a real change
// Afterwards the host is the truth, even when it moves the circle back
handle.update([52, 4], 100);
expect(handle.center).toEqual([52, 4]);
expect(handle.radius).toBe(100);
});
it("activates the center by click, Enter and Space, but not after a drag", async () => {
-50
View File
@@ -1,50 +0,0 @@
import { describe, expect, it } from "vitest";
import { getServiceCallEntityIds } from "../../src/data/service";
describe("getServiceCallEntityIds", () => {
it("returns an empty list when no entities are targeted", () => {
expect(getServiceCallEntityIds()).toEqual([]);
expect(getServiceCallEntityIds({ brightness: 50 }, {})).toEqual([]);
expect(getServiceCallEntityIds({}, { area_id: "kitchen" })).toEqual([]);
});
it("reads a single entity from target or service data", () => {
expect(getServiceCallEntityIds({}, { entity_id: "light.a" })).toEqual([
"light.a",
]);
expect(getServiceCallEntityIds({ entity_id: "light.a" })).toEqual([
"light.a",
]);
});
it("prefers the target over the legacy service data entity ids", () => {
expect(
getServiceCallEntityIds(
{ entity_id: ["light.a", "light.b"] },
{ entity_id: ["light.b", "light.c"] }
)
).toEqual(["light.b", "light.c"]);
});
it("deduplicates entity ids", () => {
expect(
getServiceCallEntityIds({}, { entity_id: ["light.a", "light.a"] })
).toEqual(["light.a"]);
});
it("splits comma separated ids and lowercases them like Core does", () => {
expect(
getServiceCallEntityIds({}, { entity_id: "Light.A, light.b ,light.a" })
).toEqual(["light.a", "light.b"]);
});
it("ignores wildcard, malformed, and non-string entity ids", () => {
expect(getServiceCallEntityIds({ entity_id: "all" })).toEqual([]);
expect(
getServiceCallEntityIds(
{},
{ entity_id: ["all", "none", "", "light", 5] as unknown as string[] }
)
).toEqual([]);
});
});
+15 -15
View File
@@ -1,15 +1,4 @@
import { expect, test } from "@playwright/test";
import {
activateDemoSidebarPanel,
DEMO_THEME_STORAGE_KEY,
demoCardSelector,
expectDemoDarkMode,
expectStoredDemoTheme,
loadDemo,
moreInfoCardSelector,
openDemoSidebar,
reloadDemo,
} from "./demo/helpers";
import {
expectNoPageErrors,
NAVIGATION_TIMEOUT,
@@ -18,6 +7,17 @@ import {
SHELL_TIMEOUT,
trackPageErrors,
} from "./helpers";
import {
DEMO_THEME_STORAGE_KEY,
activateDemoSidebarPanel,
demoCardSelector,
expectDemoDarkMode,
expectStoredDemoTheme,
loadDemo,
moreInfoCardSelector,
openDemoSidebar,
reloadDemo,
} from "./demo/helpers";
test.describe("Home Assistant Demo", () => {
let pageErrors: ReturnType<typeof trackPageErrors>;
@@ -82,10 +82,10 @@ test.describe("Home Assistant Demo", () => {
// Adapters come from the config entries, connections and advertisements
// from the subscriptions. A count of zero means the data never arrived.
const rows = dashboard.locator("ha-list-item-button div[slot='headline']");
const rows = dashboard.locator("ha-md-list-item div[slot='headline']");
await expect(rows).toHaveCount(3, { timeout: PANEL_TIMEOUT });
// Asserted over the list as a whole so a single empty count still fails.
await expect(dashboard.locator("ha-list-nav")).not.toHaveText(/\b0\b/, {
await expect(dashboard.locator("ha-md-list")).not.toHaveText(/\b0\b/, {
timeout: QUICK_TIMEOUT,
});
@@ -96,7 +96,7 @@ test.describe("Home Assistant Demo", () => {
await loadDemo(page, "/#/config/bluetooth/adapter-info");
const adapters = page.locator(
"bluetooth-adapter-info-page ha-list-item-button"
"bluetooth-adapter-info-page ha-md-list-item"
);
await expect(adapters).toHaveCount(3, { timeout: PANEL_TIMEOUT });
await expect(adapters.locator("ha-icon-button")).toHaveCount(1, {
@@ -120,7 +120,7 @@ test.describe("Home Assistant Demo", () => {
});
// The device and entity counts come from the registries; a zero means the
// fixtures did not reach them.
await expect(dashboard.locator("ha-list-nav").first()).not.toHaveText(
await expect(dashboard.locator("ha-md-list").first()).not.toHaveText(
/\b0\b/,
{ timeout: QUICK_TIMEOUT }
);
+10 -10
View File
@@ -2798,12 +2798,12 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.5.18":
version: 3.5.18
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.18"
"@formatjs/icu-messageformat-parser@npm:3.5.17":
version: 3.5.17
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.17"
dependencies:
"@formatjs/icu-skeleton-parser": "npm:2.1.11"
checksum: 10/fc2215a16571fcff3eb9f446878ef7649d40064a79648352ab79f68f4b48ce7b196125495eec82caa02edec78a9a094d8fd6fd5db0abeb353b95afb62672c2a2
checksum: 10/cbb9daf23f65e4ef3697eae3be4c6888eda942fcde18929dfc6e96ef6c45052409d20340ece13476eeadd062e222809c46a2f26e3b453c9cbd4979bb6c0b0ae5
languageName: node
linkType: hard
@@ -10472,7 +10472,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.3.0"
intl-messageformat: "npm:11.2.15"
intl-messageformat: "npm:11.2.14"
js-yaml: "npm:5.4.1"
jsdom: "npm:30.0.1"
jszip: "npm:3.10.2"
@@ -10879,13 +10879,13 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:11.2.15":
version: 11.2.15
resolution: "intl-messageformat@npm:11.2.15"
"intl-messageformat@npm:11.2.14":
version: 11.2.14
resolution: "intl-messageformat@npm:11.2.14"
dependencies:
"@formatjs/fast-memoize": "npm:3.1.7"
"@formatjs/icu-messageformat-parser": "npm:3.5.18"
checksum: 10/71d1448964e685c821b3680a89668aae3fe92f6b7774842ea5eb08830417846d4fcbcaf691930e4da1ceb2cc414eeab468c7e3dd4eb530a38ffed91530468ac5
"@formatjs/icu-messageformat-parser": "npm:3.5.17"
checksum: 10/2720155c42bfd99a00d41f7bc9e2f5c2c83c2b6cb2353006b7d8e736a0f1ed20d0147406c1aeba0748731a0ce588c67304b52eb2fe92b42b928bf1ef6bffdcdf
languageName: node
linkType: hard