Add inline features position for tile card (#24199)

* Add side features position for tile card

* Add translations

* Rename to inline

* Simplify editor with 2 dropdowns

* Use 50% width

* Update src/translations/en.json

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
Paul Bottein 2025-02-18 06:33:17 +01:00 committed by GitHub
parent 94a5e737cc
commit 0b64861297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 150 additions and 39 deletions

View File

@ -20,40 +20,31 @@ export class HuiCardFeatures extends LitElement {
return nothing; return nothing;
} }
return html` return html`
<div class="container"> ${this.features.map(
${this.features.map( (feature) => html`
(feature) => html` <hui-card-feature
<hui-card-feature .hass=${this.hass}
.hass=${this.hass} .stateObj=${this.stateObj}
.stateObj=${this.stateObj} .color=${this.color}
.color=${this.color} .feature=${feature}
.feature=${feature} ></hui-card-feature>
></hui-card-feature> `
` )}
)}
</div>
`; `;
} }
static styles = css` static styles = css`
:host { :host {
--feature-color: var(--state-icon-color); --feature-color: var(--state-icon-color);
--feature-padding: 12px;
--feature-height: 42px; --feature-height: 42px;
--feature-border-radius: 12px; --feature-border-radius: 12px;
--feature-button-spacing: 12px; --feature-button-spacing: 12px;
position: relative; position: relative;
width: 100%; width: 100%;
}
.container {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: var(--feature-padding); gap: 12px;
padding-top: 0px;
gap: var(--feature-padding);
width: 100%; width: 100%;
height: 100%;
box-sizing: border-box; box-sizing: border-box;
justify-content: space-evenly; justify-content: space-evenly;
} }

View File

@ -256,6 +256,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
hui-card-features { hui-card-features {
width: 100%; width: 100%;
flex: none; flex: none;
padding: 0 12px 12px 12px;
} }
`; `;
} }

View File

@ -248,6 +248,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
hui-card-features { hui-card-features {
width: 100%; width: 100%;
flex: none; flex: none;
padding: 0 12px 12px 12px;
} }
`; `;
} }

View File

@ -100,10 +100,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
} }
public getCardSize(): number { public getCardSize(): number {
const featuresPosition =
this._config && this._featurePosition(this._config);
const featuresCount = this._config?.features?.length || 0;
return ( return (
1 + 1 +
(this._config?.vertical ? 1 : 0) + (this._config?.vertical ? 1 : 0) +
(this._config?.features?.length || 0) (featuresPosition === "inline" ? 0 : featuresCount)
); );
} }
@ -111,9 +114,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const columns = 6; const columns = 6;
let min_columns = 6; let min_columns = 6;
let rows = 1; let rows = 1;
if (this._config?.features?.length) { const featurePosition = this._config && this._featurePosition(this._config);
rows += this._config.features.length; const featuresCount = this._config?.features?.length || 0;
if (featuresCount) {
if (featurePosition === "inline") {
min_columns = 12;
} else {
rows += featuresCount;
}
} }
if (this._config?.vertical) { if (this._config?.vertical) {
rows++; rows++;
min_columns = 3; min_columns = 3;
@ -210,6 +220,23 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
); );
} }
private _featurePosition = memoizeOne((config: TileCardConfig) => {
if (config.vertical) {
return "bottom";
}
return config.features_position || "bottom";
});
private _displayedFeatures = memoizeOne((config: TileCardConfig) => {
const features = config.features || [];
const featurePosition = this._featurePosition(config);
if (featurePosition === "inline") {
return features.slice(0, 1);
}
return features;
});
protected render() { protected render() {
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
return nothing; return nothing;
@ -263,6 +290,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
? this._getImageUrl(stateObj) ? this._getImageUrl(stateObj)
: undefined; : undefined;
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
return html` return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}> <ha-card style=${styleMap(style)} class=${classMap({ active })}>
<div <div
@ -278,7 +311,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
> >
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple> <ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div> </div>
<div class="container"> <div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}"> <div class="content ${classMap(contentClasses)}">
<ha-tile-icon <ha-tile-icon
role=${ifDefined(this._hasIconAction ? "button" : undefined)} role=${ifDefined(this._hasIconAction ? "button" : undefined)}
@ -308,13 +341,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.secondary=${stateDisplay} .secondary=${stateDisplay}
></ha-tile-info> ></ha-tile-info>
</div> </div>
${this._config.features ${features.length > 0
? html` ? html`
<hui-card-features <hui-card-features
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
.color=${this._config.color} .color=${this._config.color}
.features=${this._config.features} .features=${features}
></hui-card-features> ></hui-card-features>
` `
: nothing} : nothing}
@ -372,6 +405,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
.container.horizontal {
flex-direction: row;
}
.content { .content {
position: relative; position: relative;
display: flex; display: flex;
@ -383,6 +420,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
pointer-events: none; pointer-events: none;
gap: 10px; gap: 10px;
} }
.container.horizontal .content {
width: 50%;
}
.vertical { .vertical {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
@ -413,6 +455,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
} }
hui-card-features { hui-card-features {
--feature-color: var(--tile-color); --feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal hui-card-features {
width: 50%;
--feature-height: 36px;
padding: 10px;
padding-inline-start: 0;
} }
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"], ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],

View File

@ -533,6 +533,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
icon_hold_action?: ActionConfig; icon_hold_action?: ActionConfig;
icon_double_tap_action?: ActionConfig; icon_double_tap_action?: ActionConfig;
features?: LovelaceCardFeatureConfig[]; features?: LovelaceCardFeatureConfig[];
features_position?: "bottom" | "inline";
} }
export interface HeadingCardConfig extends LovelaceCardConfig { export interface HeadingCardConfig extends LovelaceCardConfig {

View File

@ -8,6 +8,7 @@ import {
assert, assert,
assign, assign,
boolean, boolean,
enums,
object, object,
optional, optional,
string, string,
@ -15,6 +16,7 @@ import {
} from "superstruct"; } from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
@ -54,6 +56,7 @@ const cardConfigStruct = assign(
icon_hold_action: optional(actionConfigStruct), icon_hold_action: optional(actionConfigStruct),
icon_double_tap_action: optional(actionConfigStruct), icon_double_tap_action: optional(actionConfigStruct),
features: optional(array(any())), features: optional(array(any())),
features_position: optional(enums(["bottom", "inline"])),
}) })
); );
@ -109,8 +112,10 @@ export class HuiTileCardEditor
private _schema = memoizeOne( private _schema = memoizeOne(
( (
localize: LocalizeFunc,
entityId: string | undefined, entityId: string | undefined,
hideState: boolean, hideState: boolean,
vertical: boolean,
displayActions: AdvancedActions[] = [] displayActions: AdvancedActions[] = []
) => ) =>
[ [
@ -148,12 +153,6 @@ export class HuiTileCardEditor
boolean: {}, boolean: {},
}, },
}, },
{
name: "vertical",
selector: {
boolean: {},
},
},
{ {
name: "hide_state", name: "hide_state",
selector: { selector: {
@ -175,6 +174,43 @@ export class HuiTileCardEditor
}, },
] as const satisfies readonly HaFormSchema[]) ] as const satisfies readonly HaFormSchema[])
: []), : []),
{
name: "",
type: "grid",
schema: [
{
name: "content_layout",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["horizontal", "vertical"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
),
value,
})),
},
},
},
{
name: "features_position",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["bottom", "inline"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
),
value,
disabled: vertical && value === "inline",
})),
},
},
},
],
},
], ],
}, },
{ {
@ -223,12 +259,22 @@ export class HuiTileCardEditor
const stateObj = entityId ? this.hass!.states[entityId] : undefined; const stateObj = entityId ? this.hass!.states[entityId] : undefined;
const schema = this._schema( const schema = this._schema(
this.hass.localize,
entityId, entityId,
this._config!.hide_state ?? false, this._config.hide_state ?? false,
this._config.vertical ?? false,
this._displayActions this._displayActions
); );
const data = this._config; const data = {
...this._config,
content_layout: this._config.vertical ? "vertical" : "horizontal",
};
// Default features position to bottom and force it to bottom in vertical mode
if (!data.features_position || data.vertical) {
data.features_position = "bottom";
}
return html` return html`
<ha-form <ha-form
@ -280,6 +326,12 @@ export class HuiTileCardEditor
delete config.state_content; delete config.state_content;
} }
// Convert content_layout to vertical
if (config.content_layout) {
config.vertical = config.content_layout === "vertical";
delete config.content_layout;
}
fireEvent(this, "config-changed", { config }); fireEvent(this, "config-changed", { config });
} }
@ -337,11 +389,11 @@ export class HuiTileCardEditor
case "icon_hold_action": case "icon_hold_action":
case "icon_double_tap_action": case "icon_double_tap_action":
case "show_entity_picture": case "show_entity_picture":
case "vertical":
case "hide_state": case "hide_state":
case "state_content": case "state_content":
case "content_layout":
case "appearance": case "appearance":
case "interactions": case "features_position":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.${schema.name}` `ui.panel.lovelace.editor.card.tile.${schema.name}`
); );
@ -377,6 +429,14 @@ export class HuiTileCardEditor
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
.info {
color: var(--secondary-text-color);
margin-top: 0;
margin-bottom: 8px;
}
.features-form {
margin-bottom: 8px;
}
`, `,
]; ];
} }

View File

@ -7119,12 +7119,20 @@
"icon_tap_action": "Icon tap behavior", "icon_tap_action": "Icon tap behavior",
"icon_hold_action": "Icon hold behavior", "icon_hold_action": "Icon hold behavior",
"icon_double_tap_action": "Icon double tap behavior", "icon_double_tap_action": "Icon double tap behavior",
"interactions": "Interactions",
"appearance": "Appearance", "appearance": "Appearance",
"show_entity_picture": "Show entity picture", "show_entity_picture": "Show entity picture",
"vertical": "Vertical",
"hide_state": "Hide state", "hide_state": "Hide state",
"state_content": "State content" "state_content": "State content",
"features_position": "Features position",
"features_position_options": {
"bottom": "Bottom",
"inline": "Inline"
},
"content_layout": "Content layout",
"content_layout_options": {
"horizontal": "Horizontal",
"vertical": "Vertical"
}
}, },
"vertical-stack": { "vertical-stack": {
"name": "Vertical stack", "name": "Vertical stack",