Compare commits

...

1 Commits

Author SHA1 Message Date
Paul Bottein 856086e4e8 Add responsive column layout to device and area config pages 2026-06-15 12:05:32 +02:00
3 changed files with 501 additions and 450 deletions
+30
View File
@@ -0,0 +1,30 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { ReactiveControllerHost } from "lit";
import { clamp } from "../number/clamp";
// Count columns from the container's real width (not the viewport) so a
// docked sidebar is accounted for, like the dashboard sections view.
const MIN_COLUMN_WIDTH = 320;
const DEFAULT_COLUMN_GAP = 16;
const parsePx = (value: string) => parseInt(value, 10) || 0;
export const createColumnsController = (
host: ReactiveControllerHost & Element,
maxColumns: number
) =>
new ResizeController<number>(host, {
target: null,
skipInitial: true,
callback: (entries) => {
const entry = entries[0];
if (!entry) {
return maxColumns;
}
const width = entry.contentRect.width;
const gap =
parsePx(getComputedStyle(entry.target).columnGap) || DEFAULT_COLUMN_GAP;
const columns = Math.floor((width + gap) / (MIN_COLUMN_WIDTH + gap));
return clamp(columns, 1, maxColumns);
},
});
+283 -272
View File
@@ -34,6 +34,7 @@ import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
import { afterNextRender } from "../../../common/util/render-status";
import { createColumnsController } from "../../../common/util/responsive-columns";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
@@ -139,6 +140,8 @@ const NAVIGATION_ACTIONS: {
},
] as const;
const MAX_COLUMNS = 3;
@customElement("ha-config-area-page")
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -159,6 +162,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
private _logbookTime = { recent: 86400 };
private _columnsController = createColumnsController(this, MAX_COLUMNS);
private _memberships = memoizeOne(
(
areaId: string,
@@ -357,6 +362,267 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
)
);
const infoColumn = html`
${area.picture
? html`<div class="img-container">
<img alt=${area.name} src=${area.picture} />
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize(
"ui.panel.config.areas.edit_settings"
)}
class="img-edit-btn"
></ha-icon-button>
</div>`
: nothing}
${area.picture && !this._newTriggersConditions
? nothing
: html`<div class="action-buttons">
${area.picture
? nothing
: html`<ha-button
appearance="filled"
.entry=${area}
@click=${this._showSettings}
>
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.add_picture")}
</ha-button>`}
${this._newTriggersConditions
? html`<ha-button
appearance="filled"
variant="brand"
@click=${this._showAddToDialog}
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.item"
)}
</ha-button>`
: nothing}
</div>`}
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.devices.caption")}
>${devices.length
? html`<ha-list>
${devices.map(
(device) => html`
<a href="/config/devices/device/${device.id}">
<ha-list-item hasMeta>
<span>${device.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`
)}
</ha-list>`
: html`
<div class="no-entries">
${this.hass.localize("ui.panel.config.devices.no_devices")}
</div>
`}
</ha-card>
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption"
)}
>
${nonAutomatedEntities.length
? html`<ha-list>
${nonAutomatedEntities.map(
(entity) => html`
<ha-list-item
@click=${this._openEntity}
.entity=${entity}
hasMeta
>
<span>${entity.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}</ha-list
>`
: html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.areas.editor.no_linked_entities"
)}
</div>
`}
</ha-card>
`;
const relatedColumn = html`
${isComponentLoaded(this.hass.config, "automation")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
>
${groupedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
<ha-list>
${groupedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}</ha-list
>`
: ""}
${relatedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
<ha-list>
${relatedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}</ha-list
>`
: ""}
${!groupedAutomations?.length && !relatedAutomations?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "scene")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading"
)}
>
${groupedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
<ha-list>
${groupedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}</ha-list
>`
: ""}
${relatedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
<ha-list>
${relatedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}</ha-list
>`
: ""}
${!groupedScenes?.length && !relatedScenes?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "script")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading"
)}
>
${groupedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
${groupedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${relatedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
${relatedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${!groupedScripts?.length && !relatedScripts?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.script.no_scripts"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
`;
const logbookColumn = html`
${isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card outlined .header=${this.hass.localize("panel.logbook")}>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""}
`;
// In 2 columns the logbook goes on the right, under the shorter
// automations/scenes/scripts column, to balance the column heights.
const columns =
this._columnsController.value ?? (this.narrow ? 1 : MAX_COLUMNS);
const columnContents =
columns >= 3
? [[infoColumn], [relatedColumn], [logbookColumn]]
: columns === 2
? [[infoColumn], [relatedColumn, logbookColumn]]
: [[infoColumn, relatedColumn, logbookColumn]];
return html`
<hass-subpage
.hass=${this.hass}
@@ -401,266 +667,10 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
<div class="container">
<div class="column">
${area.picture
? html`<div class="img-container">
<img alt=${area.name} src=${area.picture} />
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize(
"ui.panel.config.areas.edit_settings"
)}
class="img-edit-btn"
></ha-icon-button>
</div>`
: nothing}
${area.picture && !this._newTriggersConditions
? nothing
: html`<div class="action-buttons">
${area.picture
? nothing
: html`<ha-button
appearance="filled"
.entry=${area}
@click=${this._showSettings}
>
<ha-svg-icon
.path=${mdiImagePlus}
slot="start"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.add_picture"
)}
</ha-button>`}
${this._newTriggersConditions
? html`<ha-button
appearance="filled"
variant="brand"
@click=${this._showAddToDialog}
>
<ha-svg-icon
slot="start"
.path=${mdiPlus}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.item"
)}
</ha-button>`
: nothing}
</div>`}
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.devices.caption")}
>${devices.length
? html`<ha-list>
${devices.map(
(device) => html`
<a href="/config/devices/device/${device.id}">
<ha-list-item hasMeta>
<span>${device.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`
)}
</ha-list>`
: html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.no_devices"
)}
</div>
`}
</ha-card>
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption"
)}
>
${nonAutomatedEntities.length
? html`<ha-list>
${nonAutomatedEntities.map(
(entity) => html`
<ha-list-item
@click=${this._openEntity}
.entity=${entity}
hasMeta
>
<span>${entity.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}</ha-list
>`
: html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.areas.editor.no_linked_entities"
)}
</div>
`}
</ha-card>
</div>
<div class="column">
${isComponentLoaded(this.hass.config, "automation")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
>
${groupedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
<ha-list>
${groupedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}</ha-list
>`
: ""}
${relatedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
<ha-list>
${relatedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}</ha-list
>`
: ""}
${!groupedAutomations?.length && !relatedAutomations?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "scene")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading"
)}
>
${groupedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
<ha-list>
${groupedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}</ha-list
>`
: ""}
${relatedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
<ha-list>
${relatedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}</ha-list
>`
: ""}
${!groupedScenes?.length && !relatedScenes?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "script")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading"
)}
>
${groupedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
${groupedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${relatedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
${relatedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${!groupedScripts?.length && !relatedScripts?.length
? html`
<div class="no-entries">
${this.hass.localize(
"ui.panel.config.devices.script.no_scripts"
)}
</div>
`
: ""}
</ha-card>
`
: ""}
</div>
<div class="column">
${isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card
outlined
.header=${this.hass.localize("panel.logbook")}
>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""}
</div>
<div class="container" ${this._columnsController.target()}>
${columnContents.map(
(contents) => html`<div class="column">${contents}</div>`
)}
</div>
</hass-subpage>
`;
@@ -904,30 +914,31 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
width: 100%;
}
:host {
display: block;
}
.container {
display: flex;
flex-wrap: wrap;
gap: var(--ha-space-4);
margin: auto;
max-width: 1000px;
margin-top: 32px;
margin-bottom: 32px;
max-width: 1280px;
box-sizing: border-box;
padding: var(--ha-space-2) var(--ha-space-4);
margin-top: var(--ha-space-8);
margin-bottom: var(--ha-space-8);
}
.column {
padding: 8px;
box-sizing: border-box;
width: 33%;
flex-grow: 1;
flex: 1 1 0;
min-width: 0;
}
.fullwidth {
padding: 8px;
padding: var(--ha-space-2);
width: 100%;
}
.column > *:not(:first-child) {
margin-top: 16px;
}
:host([narrow]) .column {
width: 100%;
margin-top: var(--ha-space-4);
}
:host([narrow]) .container {
+188 -178
View File
@@ -38,6 +38,7 @@ import { stringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { computeRTL } from "../../../common/util/compute_rtl";
import { groupBy } from "../../../common/util/group-by";
import { createColumnsController } from "../../../common/util/responsive-columns";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -175,6 +176,8 @@ export interface DeviceAlert {
const DEVICE_ALERTS_INTERVAL = 30000;
const MAX_COLUMNS = 3;
@customElement("ha-config-device-page")
export class HaConfigDevicePage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -211,6 +214,8 @@ export class HaConfigDevicePage extends LitElement {
private _logbookTime = { recent: 86400 };
private _columnsController = createColumnsController(this, MAX_COLUMNS);
private _integrations = memoizeOne(
(
device: DeviceRegistryEntry,
@@ -749,6 +754,176 @@ export class HaConfigDevicePage extends LitElement {
`
: "";
const infoColumn = html`
${this._deviceAlerts?.length
? html`
<div>
${this._deviceAlerts.map(
(alert) => html`
<ha-alert .alertType=${alert.level}> ${alert.text} </ha-alert>
`
)}
</div>
`
: ""}
<ha-device-info-card .hass=${this.hass} .device=${device}>
${deviceInfo}
${firstDeviceAction || actions.length
? html`
<div class="card-actions" slot="actions">
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes("warning")
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
${actions.length
? html`
<ha-dropdown
@wa-select=${this._deviceActionSelected}
placement="bottom-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${actions.map((deviceAction, idx) => {
const dropdownItem = html`<ha-dropdown-item
.value=${idx}
.data=${deviceAction}
.variant=${deviceAction.classes?.includes("warning")
? "danger"
: "default"}
>
${deviceAction.icon
? html`
<ha-svg-icon
.path=${deviceAction.icon}
slot="icon"
></ha-svg-icon>
`
: ""}
${deviceAction.label}
${deviceAction.trailingIcon
? html`
<ha-svg-icon
slot="details"
.path=${deviceAction.trailingIcon}
></ha-svg-icon>
`
: ""}
</ha-dropdown-item>`;
return deviceAction.href
? html`<a
href=${deviceAction.href}
target=${ifDefined(deviceAction.target)}
rel=${ifDefined(
deviceAction.target ? "noreferrer" : undefined
)}
>${dropdownItem}
</a>`
: dropdownItem;
})}
</ha-dropdown>
`
: ""}
</div>
`
: ""}
</ha-device-info-card>
`;
const entitiesColumn = html`
${(
[
"control",
"sensor",
"notify",
"event",
"assist",
"config",
"diagnostic",
] as const
).map((category) =>
// Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control")
? html`
<ha-device-entities-card
.hass=${this.hass}
.header=${this.hass.localize(
`ui.panel.config.devices.entities.${category}`
)}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]}
.showHidden=${device.disabled_by !== null}
>
</ha-device-entities-card>
`
: ""
)}
<ha-device-via-devices-card
.hass=${this.hass}
.deviceId=${this.deviceId}
></ha-device-via-devices-card>
`;
const logbookColumn = isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card outlined>
<h1 class="card-header">${this.hass.localize("panel.logbook")}</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: nothing;
const columns =
this._columnsController.value ?? (this.narrow ? 1 : MAX_COLUMNS);
const columnContents =
columns >= 3
? [[infoColumn, relatedCard], [entitiesColumn], [logbookColumn]]
: columns === 2
? [[infoColumn, relatedCard, logbookColumn], [entitiesColumn]]
: [[infoColumn, entitiesColumn, relatedCard, logbookColumn]];
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
@@ -796,7 +971,7 @@ export class HaConfigDevicePage extends LitElement {
</ha-dropdown-item>
</ha-dropdown>
<div class="container">
<div class="container" ${this._columnsController.target()}>
<div class="header fullwidth">
${area
? html`<div class="header-name">
@@ -849,175 +1024,9 @@ export class HaConfigDevicePage extends LitElement {
: ""}
</div>
</div>
<div class="column">
${this._deviceAlerts?.length
? html`
<div>
${this._deviceAlerts.map(
(alert) => html`
<ha-alert .alertType=${alert.level}>
${alert.text}
</ha-alert>
`
)}
</div>
`
: ""}
<ha-device-info-card .hass=${this.hass} .device=${device}>
${deviceInfo}
${firstDeviceAction || actions.length
? html`
<div class="card-actions" slot="actions">
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes("warning")
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
${actions.length
? html`
<ha-dropdown
@wa-select=${this._deviceActionSelected}
placement="bottom-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${actions.map((deviceAction, idx) => {
const dropdownItem = html`<ha-dropdown-item
.value=${idx}
.data=${deviceAction}
.variant=${deviceAction.classes?.includes(
"warning"
)
? "danger"
: "default"}
>
${deviceAction.icon
? html`
<ha-svg-icon
.path=${deviceAction.icon}
slot="icon"
></ha-svg-icon>
`
: ""}
${deviceAction.label}
${deviceAction.trailingIcon
? html`
<ha-svg-icon
slot="details"
.path=${deviceAction.trailingIcon}
></ha-svg-icon>
`
: ""}
</ha-dropdown-item>`;
return deviceAction.href
? html`<a
href=${deviceAction.href}
target=${ifDefined(deviceAction.target)}
rel=${ifDefined(
deviceAction.target
? "noreferrer"
: undefined
)}
>${dropdownItem}
</a>`
: dropdownItem;
})}
</ha-dropdown>
`
: ""}
</div>
`
: ""}
</ha-device-info-card>
${!this.narrow ? relatedCard : ""}
</div>
<div class="column">
${(
[
"control",
"sensor",
"notify",
"event",
"assist",
"config",
"diagnostic",
] as const
).map((category) =>
// Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control")
? html`
<ha-device-entities-card
.hass=${this.hass}
.header=${this.hass.localize(
`ui.panel.config.devices.entities.${category}`
)}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]}
.showHidden=${device.disabled_by !== null}
>
</ha-device-entities-card>
`
: ""
)}
<ha-device-via-devices-card
.hass=${this.hass}
.deviceId=${this.deviceId}
></ha-device-via-devices-card>
</div>
<div class="column">
${this.narrow ? relatedCard : ""}
${isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card outlined>
<h1 class="card-header">
${this.hass.localize("panel.logbook")}
</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""}
</div>
${columnContents.map(
(contents) => html`<div class="column">${contents}</div>`
)}
</div>
</hass-subpage>`;
}
@@ -1627,11 +1636,17 @@ export class HaConfigDevicePage extends LitElement {
return [
haStyle,
css`
:host {
display: block;
}
.container {
display: flex;
flex-wrap: wrap;
gap: var(--ha-space-4);
margin: auto;
max-width: 1000px;
max-width: 1280px;
box-sizing: border-box;
padding: var(--ha-space-2) var(--ha-space-4);
margin-top: var(--ha-space-8);
margin-bottom: var(--ha-space-8);
}
@@ -1692,12 +1707,11 @@ export class HaConfigDevicePage extends LitElement {
.column,
.fullwidth {
padding: var(--ha-space-2);
box-sizing: border-box;
}
.column {
width: 33%;
flex-grow: 1;
flex: 1 1 0;
min-width: 0;
}
.fullwidth {
width: 100%;
@@ -1739,10 +1753,6 @@ export class HaConfigDevicePage extends LitElement {
margin-top: var(--ha-space-4);
}
:host([narrow]) .column {
width: 100%;
}
a {
text-decoration: none;
color: var(--primary-color);