@matter-server/dashboard 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +2 -2
  2. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  3. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  4. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts +36 -0
  5. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -0
  6. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +159 -0
  7. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +6 -0
  8. package/dist/esm/pages/cluster-commands/index.d.ts +1 -0
  9. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  10. package/dist/esm/pages/cluster-commands/index.js +1 -0
  11. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  12. package/dist/esm/pages/components/footer.d.ts.map +1 -1
  13. package/dist/esm/pages/components/footer.js +4 -7
  14. package/dist/esm/pages/components/footer.js.map +1 -1
  15. package/dist/esm/pages/components/header.d.ts +5 -0
  16. package/dist/esm/pages/components/header.d.ts.map +1 -1
  17. package/dist/esm/pages/components/header.js +75 -0
  18. package/dist/esm/pages/components/header.js.map +1 -1
  19. package/dist/esm/pages/components/node-details.js +1 -1
  20. package/dist/esm/pages/components/node-details.js.map +1 -1
  21. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  22. package/dist/esm/pages/components/server-details.js +0 -1
  23. package/dist/esm/pages/components/server-details.js.map +1 -1
  24. package/dist/esm/pages/matter-dashboard-app.d.ts +12 -0
  25. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  26. package/dist/esm/pages/matter-dashboard-app.js +84 -4
  27. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  28. package/dist/esm/pages/matter-network-view.d.ts +52 -0
  29. package/dist/esm/pages/matter-network-view.d.ts.map +1 -0
  30. package/dist/esm/pages/matter-network-view.js +309 -0
  31. package/dist/esm/pages/matter-network-view.js.map +6 -0
  32. package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
  33. package/dist/esm/pages/matter-node-view.js +70 -1
  34. package/dist/esm/pages/matter-node-view.js.map +1 -1
  35. package/dist/esm/pages/matter-server-view.d.ts +4 -0
  36. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  37. package/dist/esm/pages/matter-server-view.js +16 -1
  38. package/dist/esm/pages/matter-server-view.js.map +1 -1
  39. package/dist/esm/pages/network/base-network-graph.d.ts +74 -0
  40. package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -0
  41. package/dist/esm/pages/network/base-network-graph.js +403 -0
  42. package/dist/esm/pages/network/base-network-graph.js.map +6 -0
  43. package/dist/esm/pages/network/device-icons.d.ts +52 -0
  44. package/dist/esm/pages/network/device-icons.d.ts.map +1 -0
  45. package/dist/esm/pages/network/device-icons.js +197 -0
  46. package/dist/esm/pages/network/device-icons.js.map +6 -0
  47. package/dist/esm/pages/network/device-panel.d.ts +31 -0
  48. package/dist/esm/pages/network/device-panel.d.ts.map +1 -0
  49. package/dist/esm/pages/network/device-panel.js +183 -0
  50. package/dist/esm/pages/network/device-panel.js.map +6 -0
  51. package/dist/esm/pages/network/network-details.d.ts +47 -0
  52. package/dist/esm/pages/network/network-details.d.ts.map +1 -0
  53. package/dist/esm/pages/network/network-details.js +686 -0
  54. package/dist/esm/pages/network/network-details.js.map +6 -0
  55. package/dist/esm/pages/network/network-types.d.ts +153 -0
  56. package/dist/esm/pages/network/network-types.d.ts.map +1 -0
  57. package/dist/esm/pages/network/network-types.js +19 -0
  58. package/dist/esm/pages/network/network-types.js.map +6 -0
  59. package/dist/esm/pages/network/network-utils.d.ts +170 -0
  60. package/dist/esm/pages/network/network-utils.d.ts.map +1 -0
  61. package/dist/esm/pages/network/network-utils.js +472 -0
  62. package/dist/esm/pages/network/network-utils.js.map +6 -0
  63. package/dist/esm/pages/network/thread-graph.d.ts +27 -0
  64. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -0
  65. package/dist/esm/pages/network/thread-graph.js +134 -0
  66. package/dist/esm/pages/network/thread-graph.js.map +6 -0
  67. package/dist/esm/pages/network/wifi-graph.d.ts +27 -0
  68. package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -0
  69. package/dist/esm/pages/network/wifi-graph.js +167 -0
  70. package/dist/esm/pages/network/wifi-graph.js.map +6 -0
  71. package/dist/web/js/{commission-node-dialog-CBSDiqRW.js → commission-node-dialog-B1_khzZb.js} +5 -5
  72. package/dist/web/js/{commission-node-existing-TP6s8Tez.js → commission-node-existing-RpdajrwF.js} +2 -5
  73. package/dist/web/js/{commission-node-thread-DOB8pu6x.js → commission-node-thread-5f2itkTG.js} +2 -5
  74. package/dist/web/js/{commission-node-wifi-tzavmk1j.js → commission-node-wifi-DZ_pWqsa.js} +2 -5
  75. package/dist/web/js/{dialog-box-Dknil_Be.js → dialog-box-DEUxM4B1.js} +2 -2
  76. package/dist/web/js/{fire_event-DRpOSjJR.js → fire_event-BczBMT8E.js} +1 -1
  77. package/dist/web/js/{log-level-dialog-TXkma-7Z.js → log-level-dialog-Cr3PfX1X.js} +2 -3
  78. package/dist/web/js/main.js +1 -1
  79. package/dist/web/js/matter-dashboard-app-BuCe_Jxf.js +29990 -0
  80. package/dist/web/js/{node-binding-dialog-D52FCBFP.js → node-binding-dialog-DMiHNDLA.js} +2 -4
  81. package/dist/web/js/{prevent_default-BPgSQsuY.js → prevent_default-D4FX_PIh.js} +2 -42
  82. package/package.json +5 -4
  83. package/src/pages/cluster-commands/base-cluster-commands.ts +2 -2
  84. package/src/pages/cluster-commands/clusters/basic-information-commands.ts +171 -0
  85. package/src/pages/cluster-commands/index.ts +1 -0
  86. package/src/pages/components/footer.ts +4 -7
  87. package/src/pages/components/header.ts +81 -0
  88. package/src/pages/components/node-details.ts +2 -2
  89. package/src/pages/components/server-details.ts +0 -1
  90. package/src/pages/matter-dashboard-app.ts +105 -5
  91. package/src/pages/matter-network-view.ts +325 -0
  92. package/src/pages/matter-node-view.ts +75 -1
  93. package/src/pages/matter-server-view.ts +17 -1
  94. package/src/pages/network/base-network-graph.ts +463 -0
  95. package/src/pages/network/device-icons.ts +283 -0
  96. package/src/pages/network/device-panel.ts +180 -0
  97. package/src/pages/network/network-details.ts +750 -0
  98. package/src/pages/network/network-types.ts +161 -0
  99. package/src/pages/network/network-utils.ts +752 -0
  100. package/src/pages/network/thread-graph.ts +164 -0
  101. package/src/pages/network/wifi-graph.ts +192 -0
  102. package/dist/web/js/matter-dashboard-app-B7GUghkC.js +0 -17254
  103. package/dist/web/js/outlined-text-field-D1DyKQY-.js +0 -968
  104. package/dist/web/js/validator-C735j770.js +0 -1122
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { MatterNode } from "@matter-server/ws-client";
8
+ import {
9
+ mdiAccessPoint,
10
+ mdiCeilingLight,
11
+ mdiDoorOpen,
12
+ mdiFan,
13
+ mdiGauge,
14
+ mdiHelp,
15
+ mdiHome,
16
+ mdiLightbulb,
17
+ mdiLock,
18
+ mdiMotionSensor,
19
+ mdiPowerPlug,
20
+ mdiRouter,
21
+ mdiSpeaker,
22
+ mdiTelevision,
23
+ mdiThermometer,
24
+ mdiToggleSwitch,
25
+ mdiWater,
26
+ mdiWifi,
27
+ } from "@mdi/js";
28
+ import { ThemeService } from "../../util/theme-service.js";
29
+
30
+ /**
31
+ * Get theme-aware default icon color.
32
+ */
33
+ function getDefaultIconColor(): string {
34
+ return ThemeService.effectiveTheme === "dark" ? "#b0b0b0" : "#666666";
35
+ }
36
+
37
+ /**
38
+ * Device type IDs from Matter specification (from DeviceTypeList attribute 0/29/0).
39
+ */
40
+ const DeviceTypes = {
41
+ // Lighting
42
+ ON_OFF_LIGHT: 0x0100,
43
+ DIMMABLE_LIGHT: 0x0101,
44
+ COLOR_TEMPERATURE_LIGHT: 0x010c,
45
+ EXTENDED_COLOR_LIGHT: 0x010d,
46
+
47
+ // Plugs/Outlets
48
+ ON_OFF_PLUG: 0x010a,
49
+ DIMMABLE_PLUG: 0x010b,
50
+
51
+ // Switches
52
+ ON_OFF_SWITCH: 0x0103,
53
+ DIMMER_SWITCH: 0x0104,
54
+ COLOR_DIMMER_SWITCH: 0x0105,
55
+ GENERIC_SWITCH: 0x000f,
56
+
57
+ // Sensors
58
+ CONTACT_SENSOR: 0x0015,
59
+ OCCUPANCY_SENSOR: 0x0107,
60
+ TEMPERATURE_SENSOR: 0x0302,
61
+ HUMIDITY_SENSOR: 0x0307,
62
+ LIGHT_SENSOR: 0x0106,
63
+ PRESSURE_SENSOR: 0x0305,
64
+ FLOW_SENSOR: 0x0306,
65
+
66
+ // HVAC
67
+ THERMOSTAT: 0x0301,
68
+ FAN: 0x002b,
69
+
70
+ // Closures
71
+ DOOR_LOCK: 0x000a,
72
+ WINDOW_COVERING: 0x0202,
73
+
74
+ // Media
75
+ SPEAKER: 0x0022,
76
+ BASIC_VIDEO_PLAYER: 0x0028,
77
+ TELEVISION: 0x0023,
78
+
79
+ // Infrastructure
80
+ ROOT_NODE: 0x0016,
81
+ BRIDGE: 0x000e,
82
+ AGGREGATOR: 0x000e, // Same as bridge
83
+
84
+ // Water
85
+ WATER_LEAK_DETECTOR: 0x0043,
86
+ WATER_VALVE: 0x0042,
87
+ };
88
+
89
+ /**
90
+ * Maps device type IDs to MDI icon paths.
91
+ */
92
+ const deviceTypeToIcon: Record<number, string> = {
93
+ // Lighting
94
+ [DeviceTypes.ON_OFF_LIGHT]: mdiLightbulb,
95
+ [DeviceTypes.DIMMABLE_LIGHT]: mdiLightbulb,
96
+ [DeviceTypes.COLOR_TEMPERATURE_LIGHT]: mdiCeilingLight,
97
+ [DeviceTypes.EXTENDED_COLOR_LIGHT]: mdiCeilingLight,
98
+
99
+ // Plugs/Outlets
100
+ [DeviceTypes.ON_OFF_PLUG]: mdiPowerPlug,
101
+ [DeviceTypes.DIMMABLE_PLUG]: mdiPowerPlug,
102
+
103
+ // Switches
104
+ [DeviceTypes.ON_OFF_SWITCH]: mdiToggleSwitch,
105
+ [DeviceTypes.DIMMER_SWITCH]: mdiToggleSwitch,
106
+ [DeviceTypes.COLOR_DIMMER_SWITCH]: mdiToggleSwitch,
107
+ [DeviceTypes.GENERIC_SWITCH]: mdiToggleSwitch,
108
+
109
+ // Sensors
110
+ [DeviceTypes.CONTACT_SENSOR]: mdiDoorOpen,
111
+ [DeviceTypes.OCCUPANCY_SENSOR]: mdiMotionSensor,
112
+ [DeviceTypes.TEMPERATURE_SENSOR]: mdiThermometer,
113
+ [DeviceTypes.HUMIDITY_SENSOR]: mdiGauge,
114
+ [DeviceTypes.LIGHT_SENSOR]: mdiGauge,
115
+ [DeviceTypes.PRESSURE_SENSOR]: mdiGauge,
116
+ [DeviceTypes.FLOW_SENSOR]: mdiGauge,
117
+
118
+ // HVAC
119
+ [DeviceTypes.THERMOSTAT]: mdiThermometer,
120
+ [DeviceTypes.FAN]: mdiFan,
121
+
122
+ // Closures
123
+ [DeviceTypes.DOOR_LOCK]: mdiLock,
124
+ [DeviceTypes.WINDOW_COVERING]: mdiHome,
125
+
126
+ // Media
127
+ [DeviceTypes.SPEAKER]: mdiSpeaker,
128
+ [DeviceTypes.BASIC_VIDEO_PLAYER]: mdiTelevision,
129
+ [DeviceTypes.TELEVISION]: mdiTelevision,
130
+
131
+ // Infrastructure
132
+ [DeviceTypes.ROOT_NODE]: mdiHome,
133
+ [DeviceTypes.BRIDGE]: mdiRouter,
134
+ [DeviceTypes.AGGREGATOR]: mdiRouter,
135
+
136
+ // Water
137
+ [DeviceTypes.WATER_LEAK_DETECTOR]: mdiWater,
138
+ [DeviceTypes.WATER_VALVE]: mdiWater,
139
+ };
140
+
141
+ /**
142
+ * Maps Thread routing roles to MDI icon paths.
143
+ */
144
+ const threadRoleToIcon: Record<number, string> = {
145
+ 5: mdiRouter, // Router
146
+ 6: mdiAccessPoint, // Leader
147
+ };
148
+
149
+ /**
150
+ * Gets the primary device type ID for a node.
151
+ * Reads from DeviceTypeList attribute (0/29/0) on endpoint 1 or 0.
152
+ * The data comes as { 0: deviceTypeId, 1: revision } with numeric keys.
153
+ */
154
+ export function getPrimaryDeviceType(node: MatterNode): number | undefined {
155
+ // Check endpoint 1 first (most common for Matter devices)
156
+ const deviceTypeList1 = node.attributes["1/29/0"] as Array<Record<string, number>> | undefined;
157
+ if (deviceTypeList1?.length) {
158
+ // Device type is at key "0" (numeric key as string)
159
+ const entry = deviceTypeList1[0];
160
+ return entry?.["0"] ?? entry?.deviceType;
161
+ }
162
+
163
+ // Fall back to endpoint 0 (root node)
164
+ const deviceTypeList0 = node.attributes["0/29/0"] as Array<Record<string, number>> | undefined;
165
+ if (deviceTypeList0?.length) {
166
+ const entry = deviceTypeList0[0];
167
+ return entry?.["0"] ?? entry?.deviceType;
168
+ }
169
+
170
+ return undefined;
171
+ }
172
+
173
+ /**
174
+ * Gets the appropriate MDI icon path for a node.
175
+ * Considers device type and Thread role.
176
+ */
177
+ export function getDeviceIcon(node: MatterNode, threadRole?: number): string {
178
+ // For Thread routers/leaders, show network infrastructure icons
179
+ if (threadRole !== undefined && threadRoleToIcon[threadRole]) {
180
+ // But only if the device is primarily an infrastructure device
181
+ const deviceType = getPrimaryDeviceType(node);
182
+ if (deviceType === DeviceTypes.ROOT_NODE || deviceType === DeviceTypes.BRIDGE || node.is_bridge) {
183
+ return threadRoleToIcon[threadRole];
184
+ }
185
+ }
186
+
187
+ // Check for bridge first
188
+ if (node.is_bridge) {
189
+ return mdiRouter;
190
+ }
191
+
192
+ // Look up by device type
193
+ const deviceType = getPrimaryDeviceType(node);
194
+ if (deviceType !== undefined && deviceTypeToIcon[deviceType]) {
195
+ return deviceTypeToIcon[deviceType];
196
+ }
197
+
198
+ // Default icon
199
+ return mdiHome;
200
+ }
201
+
202
+ /**
203
+ * Gets the appropriate MDI icon path for a network type.
204
+ */
205
+ export function getNetworkTypeIcon(networkType: string): string {
206
+ switch (networkType) {
207
+ case "thread":
208
+ return mdiAccessPoint;
209
+ case "wifi":
210
+ return mdiWifi;
211
+ case "ethernet":
212
+ return mdiRouter;
213
+ default:
214
+ return mdiHome;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Creates an SVG data URL from an MDI icon path for use in vis.js.
220
+ * @param iconPath - The MDI icon path
221
+ * @param color - The icon color (CSS color string)
222
+ * @param size - The icon size in pixels
223
+ * @returns A data URL containing the SVG
224
+ */
225
+ export function createIconDataUrl(iconPath: string, color: string, size: number = 48): string {
226
+ // MDI icons use a 24x24 viewBox
227
+ const svg = `
228
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}">
229
+ <circle cx="12" cy="12" r="11" fill="white" stroke="${color}" stroke-width="1"/>
230
+ <path d="${iconPath}" fill="${color}" transform="scale(0.6) translate(8,8)"/>
231
+ </svg>
232
+ `.trim();
233
+
234
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
235
+ }
236
+
237
+ /**
238
+ * Creates an SVG data URL for a network graph node.
239
+ * @param node - The Matter node
240
+ * @param threadRole - Optional Thread routing role
241
+ * @param isSelected - Whether the node is selected
242
+ * @param isOffline - Whether the node is offline
243
+ * @returns A data URL containing the SVG
244
+ */
245
+ export function createNodeIconDataUrl(
246
+ node: MatterNode,
247
+ threadRole?: number,
248
+ isSelected: boolean = false,
249
+ isOffline: boolean = false,
250
+ ): string {
251
+ const iconPath = getDeviceIcon(node, threadRole);
252
+ let color: string;
253
+ if (isSelected) {
254
+ color = isOffline ? "#b71c1c" : "#1976d2"; // Dark red for selected+offline, blue for selected
255
+ } else if (isOffline) {
256
+ color = "#d32f2f"; // Red for offline
257
+ } else {
258
+ color = getDefaultIconColor(); // Theme-aware default
259
+ }
260
+ return createIconDataUrl(iconPath, color);
261
+ }
262
+
263
+ /**
264
+ * Creates an SVG data URL for an unknown Thread device (question mark).
265
+ * @param isRouter - Whether the device appears to be a router
266
+ * @param isSelected - Whether the node is selected
267
+ * @returns A data URL containing the SVG
268
+ */
269
+ export function createUnknownDeviceIconDataUrl(isRouter: boolean = false, isSelected: boolean = false): string {
270
+ const iconPath = isRouter ? mdiAccessPoint : mdiHelp;
271
+ const color = isSelected ? "#1976d2" : "#ff9800"; // Orange for unknown
272
+ return createIconDataUrl(iconPath, color);
273
+ }
274
+
275
+ /**
276
+ * Creates an SVG data URL for a WiFi access point/router.
277
+ * @param isSelected - Whether the node is selected
278
+ * @returns A data URL containing the SVG
279
+ */
280
+ export function createWiFiRouterIconDataUrl(isSelected: boolean = false): string {
281
+ const color = isSelected ? "#1976d2" : "#2196f3"; // Blue for WiFi AP
282
+ return createIconDataUrl(mdiWifi, color);
283
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import "@material/web/list/list";
8
+ import "@material/web/list/list-item";
9
+ import type { MatterNode } from "@matter-server/ws-client";
10
+ import { mdiChevronDown, mdiChevronRight, mdiEthernet, mdiRouter, mdiWifi } from "@mdi/js";
11
+ import { LitElement, css, html, nothing } from "lit";
12
+ import { customElement, property, state } from "lit/decorators.js";
13
+ import "../../components/ha-svg-icon";
14
+ import { getDeviceName } from "./network-utils.js";
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ "device-panel": DevicePanel;
19
+ }
20
+ }
21
+
22
+ export type PanelType = "wifi" | "ethernet" | "bridges";
23
+
24
+ @customElement("device-panel")
25
+ export class DevicePanel extends LitElement {
26
+ @property()
27
+ public type: PanelType = "wifi";
28
+
29
+ @property({ type: Array })
30
+ public nodeIds: number[] = [];
31
+
32
+ @property({ type: Object })
33
+ public nodes: Record<string, MatterNode> = {};
34
+
35
+ @property({ type: Boolean })
36
+ public expanded = true;
37
+
38
+ @state()
39
+ private _isExpanded = true;
40
+
41
+ override willUpdate(changedProperties: Map<string, unknown>): void {
42
+ if (changedProperties.has("expanded")) {
43
+ this._isExpanded = this.expanded;
44
+ }
45
+ }
46
+
47
+ private _getIcon(): string {
48
+ switch (this.type) {
49
+ case "wifi":
50
+ return mdiWifi;
51
+ case "ethernet":
52
+ return mdiEthernet;
53
+ case "bridges":
54
+ return mdiRouter;
55
+ }
56
+ }
57
+
58
+ private _getTitle(): string {
59
+ switch (this.type) {
60
+ case "wifi":
61
+ return "WiFi Devices";
62
+ case "ethernet":
63
+ return "Ethernet Devices";
64
+ case "bridges":
65
+ return "Bridges";
66
+ }
67
+ }
68
+
69
+ private _toggleExpanded(): void {
70
+ this._isExpanded = !this._isExpanded;
71
+ }
72
+
73
+ private _handleNodeClick(nodeId: number): void {
74
+ this.dispatchEvent(
75
+ new CustomEvent("node-selected", {
76
+ detail: { nodeId },
77
+ bubbles: true,
78
+ composed: true,
79
+ }),
80
+ );
81
+ }
82
+
83
+ override render() {
84
+ if (this.nodeIds.length === 0) {
85
+ return nothing;
86
+ }
87
+
88
+ return html`
89
+ <div class="panel">
90
+ <div class="header" @click=${this._toggleExpanded}>
91
+ <ha-svg-icon .path=${this._getIcon()} class="type-icon"></ha-svg-icon>
92
+ <span class="title">${this._getTitle()}</span>
93
+ <span class="count">(${this.nodeIds.length})</span>
94
+ <ha-svg-icon
95
+ .path=${this._isExpanded ? mdiChevronDown : mdiChevronRight}
96
+ class="expand-icon"
97
+ ></ha-svg-icon>
98
+ </div>
99
+ ${this._isExpanded
100
+ ? html`
101
+ <md-list class="device-list">
102
+ ${this.nodeIds.map(nodeId => {
103
+ const node = this.nodes[nodeId.toString()];
104
+ if (!node) return nothing;
105
+
106
+ return html`
107
+ <md-list-item type="button" @click=${() => this._handleNodeClick(nodeId)}>
108
+ <div slot="headline">Node ${nodeId}</div>
109
+ <div slot="supporting-text">${getDeviceName(node)}</div>
110
+ <ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
111
+ </md-list-item>
112
+ `;
113
+ })}
114
+ </md-list>
115
+ `
116
+ : nothing}
117
+ </div>
118
+ `;
119
+ }
120
+
121
+ static override styles = css`
122
+ :host {
123
+ display: block;
124
+ }
125
+
126
+ .panel {
127
+ background-color: var(--md-sys-color-surface, #fff);
128
+ border-radius: 8px;
129
+ border: 1px solid var(--md-sys-color-outline-variant, #ccc);
130
+ overflow: hidden;
131
+ }
132
+
133
+ .header {
134
+ display: flex;
135
+ align-items: center;
136
+ padding: 12px 16px;
137
+ cursor: pointer;
138
+ user-select: none;
139
+ background-color: var(--md-sys-color-surface-container, #f5f5f5);
140
+ }
141
+
142
+ .header:hover {
143
+ background-color: var(--md-sys-color-surface-container-high, #e8e8e8);
144
+ }
145
+
146
+ .type-icon {
147
+ --icon-primary-color: var(--md-sys-color-primary, #6200ee);
148
+ margin-right: 12px;
149
+ }
150
+
151
+ .title {
152
+ font-weight: 500;
153
+ color: var(--md-sys-color-on-surface, #333);
154
+ }
155
+
156
+ .count {
157
+ margin-left: 8px;
158
+ color: var(--md-sys-color-on-surface-variant, #666);
159
+ font-size: 0.875rem;
160
+ }
161
+
162
+ .expand-icon {
163
+ margin-left: auto;
164
+ --icon-primary-color: var(--md-sys-color-on-surface-variant, #666);
165
+ }
166
+
167
+ .device-list {
168
+ --md-list-item-leading-space: 16px;
169
+ --md-list-item-trailing-space: 16px;
170
+ }
171
+
172
+ md-list-item {
173
+ --md-list-item-one-line-container-height: 48px;
174
+ }
175
+
176
+ md-list-item::part(focus-ring) {
177
+ display: none;
178
+ }
179
+ `;
180
+ }