diff --git a/src/components/ha-grid-size-picker.ts b/src/components/ha-grid-size-picker.ts index dca2cbd2fbe1..453a24cb8d30 100644 --- a/src/components/ha-grid-size-picker.ts +++ b/src/components/ha-grid-size-picker.ts @@ -7,6 +7,7 @@ import { mdiRestore } from "@mdi/js"; import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../common/dom/fire_event"; import { HomeAssistant } from "../types"; +import { conditionalClamp } from "../common/number/clamp"; type GridSizeValue = { rows?: number; @@ -42,6 +43,10 @@ export class HaGridSizeEditor extends LitElement { } protected render() { + const disabledColumns = + this.columnMin !== undefined && this.columnMin === this.columnMax; + const disabledRows = + this.rowMin !== undefined && this.rowMin === this.rowMax; return html`
${!this.isDefault ? html` @@ -100,17 +107,11 @@ export class HaGridSizeEditor extends LitElement { .map((_, index) => { const row = Math.floor(index / this.columns) + 1; const column = (index % this.columns) + 1; - const disabled = - (this.rowMin !== undefined && row < this.rowMin) || - (this.rowMax !== undefined && row > this.rowMax) || - (this.columnMin !== undefined && column < this.columnMin) || - (this.columnMax !== undefined && column > this.columnMax); return html`
`; @@ -126,11 +127,16 @@ export class HaGridSizeEditor extends LitElement { _cellClick(ev) { const cell = ev.currentTarget as HTMLElement; - if (cell.getAttribute("disabled") !== null) return; const rows = Number(cell.getAttribute("data-row")); const columns = Number(cell.getAttribute("data-column")); + const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax); + const clampedColumn = conditionalClamp( + columns, + this.columnMin, + this.columnMax + ); fireEvent(this, "value-changed", { - value: { rows, columns }, + value: { rows: clampedRow, columns: clampedColumn }, }); } diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 160e5c427747..f7492d0f65a1 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -145,9 +145,16 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { this._config?.show_icon && (this._config?.show_name || this._config?.show_state) ) { - return { grid_rows: 2, grid_columns: 2 }; + return { + grid_rows: 2, + grid_columns: 2, + grid_min_rows: 2, + }; } - return { grid_rows: 1, grid_columns: 1 }; + return { + grid_rows: 1, + grid_columns: 1, + }; } public setConfig(config: ButtonCardConfig): void { diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index e6366cd28b6c..42861e70fe4e 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -36,7 +36,11 @@ import { findEntities } from "../common/find-entities"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; -import { LovelaceCard, LovelaceHeaderFooter } from "../types"; +import { + LovelaceCard, + LovelaceHeaderFooter, + LovelaceLayoutOptions, +} from "../types"; import { HuiErrorCard } from "./hui-error-card"; import { EntityCardConfig } from "./types"; @@ -241,6 +245,15 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); } + public getLayoutOptions(): LovelaceLayoutOptions { + return { + grid_columns: 2, + grid_rows: 2, + grid_min_columns: 2, + grid_min_rows: 2, + }; + } + static get styles(): CSSResultGroup { return [ iconColorCSS, diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index f964148c0a64..405158847a09 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { + LovelaceCard, + LovelaceCardEditor, + LovelaceLayoutOptions, +} from "../types"; import { HumidifierCardConfig } from "./types"; @customElement("hui-humidifier-card") @@ -173,6 +177,24 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { `; } + public getLayoutOptions(): LovelaceLayoutOptions { + const grid_columns = 4; + let grid_rows = 5; + let grid_min_rows = 2; + const grid_min_columns = 2; + if (this._config?.features?.length) { + const featureHeight = Math.ceil((this._config.features.length * 2) / 3); + grid_rows += featureHeight; + grid_min_rows += featureHeight; + } + return { + grid_columns, + grid_rows, + grid_min_rows, + grid_min_columns, + }; + } + static get styles(): CSSResultGroup { return css` :host { diff --git a/src/panels/lovelace/cards/hui-iframe-card.ts b/src/panels/lovelace/cards/hui-iframe-card.ts index c87766cbae64..36cdea605ec3 100644 --- a/src/panels/lovelace/cards/hui-iframe-card.ts +++ b/src/panels/lovelace/cards/hui-iframe-card.ts @@ -119,6 +119,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard { return { grid_columns: 4, grid_rows: 4, + grid_min_rows: 2, }; } diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index ef0d25be529e..0c810280c29c 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -432,6 +432,8 @@ class HuiMapCard extends LitElement implements LovelaceCard { return { grid_columns: 4, grid_rows: 4, + grid_min_columns: 2, + grid_min_rows: 2, }; } diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index ffdfe2c34a2f..4cd3e0f1058d 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -40,7 +40,11 @@ import { findEntities } from "../common/find-entities"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-marquee"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import type { LovelaceCard, LovelaceCardEditor } from "../types"; +import type { + LovelaceCard, + LovelaceCardEditor, + LovelaceLayoutOptions, +} from "../types"; import { MediaControlCardConfig } from "./types"; @customElement("hui-media-control-card") @@ -582,6 +586,15 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } } + public getLayoutOptions(): LovelaceLayoutOptions { + return { + grid_columns: 4, + grid_min_columns: 2, + grid_rows: 3, + grid_min_rows: 3, + }; + } + static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/panels/lovelace/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts index 76ba0fa63626..c456d427b6ab 100644 --- a/src/panels/lovelace/cards/hui-sensor-card.ts +++ b/src/panels/lovelace/cards/hui-sensor-card.ts @@ -76,6 +76,8 @@ class HuiSensorCard extends HuiEntityCard { return { grid_columns: 2, grid_rows: 2, + grid_min_columns: 2, + grid_min_rows: 2, }; } diff --git a/src/panels/lovelace/cards/hui-statistic-card.ts b/src/panels/lovelace/cards/hui-statistic-card.ts index e5beb4a209cc..62338e7a65f3 100644 --- a/src/panels/lovelace/cards/hui-statistic-card.ts +++ b/src/panels/lovelace/cards/hui-statistic-card.ts @@ -1,10 +1,10 @@ import { HassEntity } from "home-assistant-js-websocket"; import { - css, CSSResultGroup, - html, LitElement, PropertyValues, + css, + html, nothing, } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -16,12 +16,12 @@ import "../../../components/ha-alert"; import "../../../components/ha-card"; import "../../../components/ha-state-icon"; import { + StatisticsMetaData, fetchStatistic, getDisplayUnit, getStatisticLabel, getStatisticMetadata, isExternalStatistic, - StatisticsMetaData, } from "../../../data/recorder"; import { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; @@ -32,6 +32,7 @@ import { LovelaceCard, LovelaceCardEditor, LovelaceHeaderFooter, + LovelaceLayoutOptions, } from "../types"; import { HuiErrorCard } from "./hui-error-card"; import { EntityCardConfig, StatisticCardConfig } from "./types"; @@ -254,6 +255,15 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard { fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); } + public getLayoutOptions(): LovelaceLayoutOptions { + return { + grid_columns: 2, + grid_rows: 2, + grid_min_columns: 2, + grid_min_rows: 2, + }; + } + static get styles(): CSSResultGroup { return [ css` diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index d8d58ee9c776..620f17a20562 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { + LovelaceCard, + LovelaceCardEditor, + LovelaceLayoutOptions, +} from "../types"; import { ThermostatCardConfig } from "./types"; @customElement("hui-thermostat-card") @@ -165,6 +169,24 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { `; } + public getLayoutOptions(): LovelaceLayoutOptions { + const grid_columns = 4; + let grid_rows = 5; + let grid_min_rows = 2; + const grid_min_columns = 2; + if (this._config?.features?.length) { + const featureHeight = Math.ceil((this._config.features.length * 2) / 3); + grid_rows += featureHeight; + grid_min_rows += featureHeight; + } + return { + grid_columns, + grid_rows, + grid_min_rows, + grid_min_columns, + }; + } + static get styles(): CSSResultGroup { return css` :host { diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index fa8c2cc6d067..19a05e3bb4ab 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -122,17 +122,21 @@ export class HuiTileCard extends LitElement implements LovelaceCard { } public getLayoutOptions(): LovelaceLayoutOptions { - const options = { - grid_columns: 2, - grid_rows: 1, - }; + const grid_columns = 2; + let grid_rows = 1; if (this._config?.features?.length) { - options.grid_rows += Math.ceil((this._config.features.length * 2) / 3); + const featureHeight = Math.ceil((this._config.features.length * 2) / 3); + grid_rows += featureHeight; } if (this._config?.vertical) { - options.grid_rows++; + grid_rows!++; } - return options; + return { + grid_columns, + grid_rows, + grid_min_rows: grid_rows, + grid_min_columns: grid_columns, + }; } private _handleAction(ev: ActionHandlerEvent) { diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 40649ed32ae2..fadca1dd5aef 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -38,7 +38,11 @@ import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import type { LovelaceCard, LovelaceCardEditor } from "../types"; +import type { + LovelaceCard, + LovelaceCardEditor, + LovelaceLayoutOptions, +} from "../types"; import type { WeatherForecastCardConfig } from "./types"; @customElement("hui-weather-forecast-card") @@ -421,6 +425,26 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { return typeof item !== "undefined" && item !== null; } + public getLayoutOptions(): LovelaceLayoutOptions { + if ( + this._config?.show_current !== false && + this._config?.show_forecast !== false + ) { + return { + grid_columns: 4, + grid_min_columns: 2, + grid_rows: 3, + grid_min_rows: 3, + }; + } + return { + grid_columns: 4, + grid_min_columns: 2, + grid_rows: 2, + grid_min_rows: 1, + }; + } + static get styles(): CSSResultGroup { return [ weatherSVGStyles, diff --git a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts index 2ef6a61f0c40..ab1bebf00fcc 100644 --- a/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts +++ b/src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts @@ -255,15 +255,14 @@ export class HaGridLayoutSlider extends LitElement { >
-
-
-
+
+
${this.value !== undefined ? html`
` @@ -323,11 +322,12 @@ export class HaGridLayoutSlider extends LitElement { position: absolute; inset: 0; background: var(--disabled-color); - opacity: 0.5; + opacity: 0.2; } .active { position: absolute; background: grey; + opacity: 0.7; top: 0; right: calc(var(--max) * 100%); bottom: 0; @@ -375,6 +375,9 @@ export class HaGridLayoutSlider extends LitElement { :host(:disabled) .slider { cursor: not-allowed; } + :host(:disabled) .handle:after { + background: var(--disabled-color); + } .pressed .handle { transition: none; } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts index 68c0487beee4..5a5690727ce6 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts @@ -19,7 +19,7 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { HuiCard } from "../../cards/hui-card"; -import { DEFAULT_GRID_OPTIONS } from "../../sections/hui-grid-section"; +import { computeSizeOnGrid } from "../../sections/hui-grid-section"; import { LovelaceLayoutOptions } from "../../types"; @customElement("hui-card-layout-editor") @@ -38,28 +38,29 @@ export class HuiCardLayoutEditor extends LitElement { private _cardElement?: HuiCard; - private _gridSizeValue = memoizeOne( + private _mergedOptions = memoizeOne( ( options?: LovelaceLayoutOptions, defaultOptions?: LovelaceLayoutOptions ) => ({ - rows: - options?.grid_rows ?? - defaultOptions?.grid_rows ?? - DEFAULT_GRID_OPTIONS.grid_rows, - columns: - options?.grid_columns ?? - defaultOptions?.grid_columns ?? - DEFAULT_GRID_OPTIONS.grid_columns, + ...defaultOptions, + ...options, }) ); + private _gridSizeValue = memoizeOne(computeSizeOnGrid); + private _isDefault = memoizeOne( (options?: LovelaceLayoutOptions) => options?.grid_columns === undefined && options?.grid_rows === undefined ); render() { + const options = this._mergedOptions( + this.config.layout_options, + this._defaultLayoutOptions + ); + return html`

@@ -123,12 +124,13 @@ export class HuiCardLayoutEditor extends LitElement { : html` `} `; diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index 8b5b8d9bb16c..0cc5c74824c0 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -15,6 +15,7 @@ import { HuiCard } from "../cards/hui-card"; import "../components/hui-card-edit-mode"; import { moveCard } from "../editor/config-util"; import type { Lovelace, LovelaceLayoutOptions } from "../types"; +import { conditionalClamp } from "../../../common/number/clamp"; const CARD_SORTABLE_OPTIONS: HaSortableOptions = { delay: 100, @@ -23,9 +24,41 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = { invertedSwapThreshold: 0.7, } as HaSortableOptions; -export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = { +export const DEFAULT_GRID_OPTIONS = { grid_columns: 4, grid_rows: 1, +} as const satisfies LovelaceLayoutOptions; + +type GridSizeValue = { + rows?: number; + columns?: number; +}; + +export const computeSizeOnGrid = ( + options: LovelaceLayoutOptions +): GridSizeValue => { + const rows = + typeof options.grid_rows === "number" + ? conditionalClamp( + options.grid_rows, + options.grid_min_rows, + options.grid_max_rows + ) + : DEFAULT_GRID_OPTIONS.grid_rows; + + const columns = + typeof options.grid_columns === "number" + ? conditionalClamp( + options.grid_columns, + options.grid_min_columns, + options.grid_max_columns + ) + : DEFAULT_GRID_OPTIONS.grid_columns; + + return { + rows, + columns, + }; }; export class GridSection extends LitElement implements LovelaceSectionElement { @@ -101,15 +134,13 @@ export class GridSection extends LitElement implements LovelaceSectionElement { card.layout = "grid"; const layoutOptions = card.getLayoutOptions(); - const columnSize = - layoutOptions.grid_columns ?? DEFAULT_GRID_OPTIONS.grid_columns; - const rowSize = - layoutOptions.grid_rows ?? DEFAULT_GRID_OPTIONS.grid_rows; + const { rows, columns } = computeSizeOnGrid(layoutOptions); + return html`